100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Aggregate and Custom Accumulators

Learn how LINQ's Aggregate() operator implements a general-purpose fold, letting you build custom accumulation logic beyond Sum, Count, and Average.

Aggregation and SetsIntermediate10 min readJul 10, 2026
Analogies

Aggregate as a General-Purpose Fold

Aggregate() is LINQ's implementation of the classic functional-programming 'fold' or 'reduce' operation: it walks the sequence one element at a time, threading an accumulator value through a function you supply, func(accumulator, element) => newAccumulator, and returns the final accumulator once the sequence is exhausted. The simplest overload, Aggregate(func), uses the first element as the seed and starts folding from the second element, which means it throws InvalidOperationException on an empty sequence since there's no first element to seed with.

🏏

Cricket analogy: A scorer building up the running total after every single ball — starting from the first run scored and adding each subsequent delivery's runs — mirrors Aggregate() threading an accumulator through each element one at a time.

Seeded Aggregate and Result Selectors

The two-argument overload, Aggregate(seed, func), lets you supply an explicit starting value of any type — not necessarily the element type — which both avoids the empty-sequence exception (an empty source just returns the seed unchanged) and lets the accumulator be a different shape than the elements, such as folding a sequence of strings into a StringBuilder or a running Dictionary<string,int> tally. The three-argument overload, Aggregate(seed, func, resultSelector), adds a final transformation applied only once to the finished accumulator, useful when you want to fold into an internal working type (like a mutable struct or tuple) but return a cleaner public-facing shape.

🏏

Cricket analogy: Starting a team's tournament points tally at zero before a ball is bowled, so an abandoned tournament with no matches still reports '0 points' instead of erroring, mirrors how a seeded Aggregate() gracefully handles empty sequences.

When to Reach for Aggregate

Aggregate() should be your last resort, not your first instinct — for common cases like summing, counting, finding min/max, or string concatenation, the dedicated operators (Sum(), Count(), Min(), Max(), string.Join()) are more readable, often more optimized, and clearer to a future reader than an equivalent Aggregate() lambda. Reach for Aggregate() when you need genuinely custom accumulation logic that no built-in operator expresses, such as computing a running maximum-drawdown in a stock price series, building a state machine that transitions based on each element, or folding a token stream into a parsed structure.

🏏

Cricket analogy: A commentator wouldn't manually recompute a batter's strike rate ball-by-ball with custom arithmetic when the scoreboard already tracks it — but they would build custom logic for something novel like 'longest boundary drought', just as Aggregate() is reserved for logic no built-in LINQ operator covers.

csharp
var prices = new List<decimal> { 100m, 105m, 98m, 120m, 90m, 130m };

// Simple overload: seeds from the first element, throws on empty
var product = prices.Aggregate((acc, p) => acc * p);

// Seeded overload: safe on empty sequences, accumulator can differ in type
var wordCounts = new[] { "the", "quick", "the", "fox", "quick", "the" }
    .Aggregate(new Dictionary<string, int>(), (dict, word) =>
    {
        dict[word] = dict.GetValueOrDefault(word) + 1;
        return dict;
    });

// Three-argument overload: custom internal state, clean public result
var maxDrawdown = prices.Aggregate(
    seed: (peak: prices[0], maxDrawdown: 0m),
    func: (state, price) =>
    {
        var peak = Math.Max(state.peak, price);
        var drawdown = (peak - price) / peak;
        return (peak, Math.Max(state.maxDrawdown, drawdown));
    },
    resultSelector: state => state.maxDrawdown);

Console.WriteLine($"Max drawdown: {maxDrawdown:P1}");

The unseeded Aggregate(func) overload throws InvalidOperationException on an empty sequence, exactly like First(), because it uses the first element as the implicit seed. If the source could plausibly be empty, always use the seeded overload with a sensible default instead.

Aggregate() is not lazy — like Sum() and Count(), it forces immediate execution and walks the entire sequence synchronously in order, so it cannot be used for infinite sequences (unlike Take() or TakeWhile(), which can short-circuit lazily).

  • Aggregate() implements a general fold/reduce, threading an accumulator through each element via a supplied function.
  • The unseeded overload uses the first element as the seed and throws on an empty sequence.
  • The seeded overload, Aggregate(seed, func), avoids the empty-sequence exception and allows a different accumulator type.
  • The three-argument overload adds a resultSelector applied once to the final accumulator, decoupling internal state from the public result.
  • Prefer built-in operators (Sum, Count, Min, Max, string.Join) over Aggregate() for standard cases — they're clearer and often faster.
  • Reach for Aggregate() only for genuinely custom, stateful accumulation logic with no dedicated operator.
  • Aggregate() forces immediate, synchronous execution across the entire sequence, so it can't handle infinite sequences.

Practice what you learned

Was this page helpful?

Topics covered

#LINQStudyNotes#MicrosoftTechnologies#AggregateAndCustomAccumulators#Aggregate#Custom#Accumulators#General#StudyNotes#SkillVeris#ExamPrep