First, rather than putting the TotalAmount on your record type, I recommend creating a new type to represent the result you're looking for.
public record Source(int Index, string No, string Path, string A, string B, string C, double Amount);
public record Totals(int Index, string No, string Path, string A, string B, string C, double Amount, double TotalAmount);
Next, I would create an intermediate representation of your data that's easier to reason with. It sounds like the combination of (A, B, C) has a meaning, and it sounds like your Path is really kind of a collection of the parent elements to the current record.
var intermediates = sources.Select(
source => new
{
source,
pathComponents = source.Path.Split('.').ToHashSet(),
abc = (source.A, source.B, source.C)
});
Then let's group those values by the combined abc value, so we can quickly look up the items that belong to the same group as a given entry.
var byAbc = intermediates.ToLookup(e => e.abc);
Finally, calculate the totals:
var totals =
from intermediate in intermediates
let totalAmount = byAbc[intermediate.abc]
.Where(e => e.pathComponents.Contains(intermediate.source.No))
.Sum(e => e.source.Amount)
let source = intermediate.source
select new Totals(
source.Index,
source.No,
source.Path,
source.A,
source.B,
source.C,
source.Amount,
totalAmount);
Here are a few of the benefits to this approach:
- By breaking down the problem into individual steps with immutable behavior:
- it's possible to step through the code and visually inspect (or log) the results from each line of code.
- it's possible to jump back to a previous step in the debugger and walk through the code again without changing the program's behavior.
- the results of the individual steps can be put into named variables that help the reader understand intent.
- the individual steps can easily be refactored out to separate methods or classes.
- By representing the path as a collection of individual pieces which can be compared using equality checks, we avoid bugs in the Contains logic. For example, if a record had a
No of 100, all of the paths in the above example would match it if you used a simple string.Contains check, even though none of the paths actually includes 100 as a component of their path.
- By using data structures like Lookup and HashSet, we avoid high asymptotic complexity, which means that this scales well to really large data sets.
- By using a different type for the input and output, you prevent the introduction of bugs in your application where someone needs to use the TotalAmount property, but is given a list where that property has not yet been populated.
Here's a LINQPad script putting the whole thing together.
Result:
