← Open in the full interactive course (progress tracking, search & more)

Filtering and grouping still leave you with sequences. Aggregation is how you collapse one down to a single answer.

"How many orders were placed?" "What's our total revenue?" "What's the most expensive product in the catalog?" None of these questions want a list back — they want one number, one value, one answer. LINQ's aggregation operators exist for exactly this: reducing a whole sequence down to a single summarizing result.

This lesson covers Count, Sum, Average, Min, Max, and Aggregate for fully custom accumulation — plus how to decide when to reach for Aggregate versus one of the specific, purpose-built aggregate methods.

What Is It?

The Simple Explanation

Every operator so far in this module — Where, Select, OrderBy, GroupBy, Join — takes a sequence and gives you back another sequence. Aggregation is different: it takes a sequence and gives you back one value. Count, Sum, Average, Min, and Max each answer one specific, common summarizing question. Aggregate is the general-purpose tool underneath all of them — a way to build your own custom reduction when none of the specific methods fit.

The Technical Definition

public static int Count<TSource>(this IEnumerable<TSource> source); public static int Count<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate); public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); public static double Average<TSource>(this IEnumerable<TSource> source, Func<TSource, int> selector); public static TSource Min<TSource>(this IEnumerable<TSource> source); public static TSource Max<TSource>(this IEnumerable<TSource> source); public static TAccumulate Aggregate<TSource, TAccumulate>( this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func)

Notice the return types: none of these return IEnumerable<T> — they return a plain int, decimal, double, TSource, or (for Aggregate) whatever accumulator type you choose. That's the defining trait of every aggregation operator, and it has a real consequence covered in Under the Hood: these methods can't be deferred the way Where or Select are — there's no sequence left to hand back, only a finished answer.

Why Does It Exist?

Before these operators, computing a total meant a manual accumulator variable and a loop:

// Manual accumulation decimal total = 0m; foreach (var order in orders) { total += order.Total; }
// Sum decimal total = orders.Sum(o => o.Total);

The manual version isn't wrong, exactly — it's just five common patterns (count, sum, average, min, max) each reimplemented from scratch, every time, with room for an off-by-one or an uninitialized variable to sneak in. Naming the intent directly — Sum, Average, Max — makes the code both shorter and unmistakably clear about what it computes.

Big Picture

SEQUENCE IN, SINGLE VALUE OUT
Input: [$120.50, $75.00, $200.00, $50.25]
↓ .Sum() → $445.75
↓ .Average() → $111.44
↓ .Max() → $200.00
↓ .Count() → 4
Every arrow starts from the same 4-item sequence — each produces one single scalar result, never a sequence.

How It Works

Aggregate, STEP BY STEP
1. START WITH THE SEED
2. FOR EACH ITEM, COMBINE IT WITH THE ACCUMULATOR
3. REPEAT FOR EVERY ITEM, IN ORDER
4. RETURN THE FINAL ACCUMULATOR VALUE

Simple Example

public record Product(int Id, string Name, string Category, decimal Price, int Stock); List<Product> products = [ new(1, "Wireless Mouse", "Electronics", 24.99m, 120), new(2, "Mechanical Keyboard", "Electronics", 89.99m, 0), new(3, "Standing Desk", "Furniture", 349.00m, 15), new(4, "Desk Lamp", "Furniture", 19.50m, 60), new(5, "Noise-Cancelling Headphones", "Electronics", 199.99m, 8), ]; int totalProducts = products.Count(); // 5 int inStockCount = products.Count(p => p.Stock > 0); // 4 decimal totalValue = products.Sum(p => p.Price * p.Stock); // total inventory value decimal averagePrice = products.Average(p => p.Price); // (decimal) — averaging decimals stays decimal decimal cheapest = products.Min(p => p.Price); // 19.50 decimal priciest = products.Max(p => p.Price); // 349.00 // Min/Max can also return the whole item, not just the compared value, via MinBy/MaxBy Product cheapestProduct = products.MinBy(p => p.Price)!; // Desk Lamp Product priciestProduct = products.MaxBy(p => p.Price)!; // Standing Desk Console.WriteLine($"{totalProducts} products, {inStockCount} in stock"); Console.WriteLine($"Average price: ${averagePrice:F2}, cheapest: {cheapestProduct.Name}, priciest: {priciestProduct.Name}"); // 5 products, 4 in stock // Average price: $136.69, cheapest: Desk Lamp, priciest: Standing Desk

Code → Meaning → Result: Min/Max with a selector return the smallest or largest value the selector produced; MinBy/MaxBy (added in .NET 6) return the whole item that produced that extreme value — genuinely useful when you need "which product" rather than just "how cheap."

Aggregate — Custom Accumulation

The five methods above cover the vast majority of everyday needs. Aggregate exists for the rest — any custom reduction that doesn't map onto count, sum, average, min, or max directly:

// Building a running total isn't a built-in operator — Aggregate handles it decimal[] amounts = [120.50m, 75.00m, 200.00m, 50.25m]; decimal total = amounts.Aggregate(0m, (running, amount) => running + amount); Console.WriteLine(total); // 445.75 — equivalent to Sum(), shown here just to illustrate the mechanics // A genuinely custom reduction: find the largest single jump between consecutive prices decimal[] prices = [24.99m, 89.99m, 349.00m, 19.50m, 199.99m]; var (_, largestJump) = prices.Aggregate( (Previous: prices[0], LargestJump: 0m), (acc, price) => { decimal jump = Math.Abs(price - acc.Previous); return (Previous: price, LargestJump: Math.Max(acc.LargestJump, jump)); }); Console.WriteLine($"Largest jump between consecutive prices: ${largestJump}"); // Largest jump between consecutive prices: $259.01 (from $89.99 to $349.00)

Here the accumulator is a tuple carrying two pieces of running state (the previous price, and the largest jump seen so far) — something none of Sum/Average/Min/Max could express, because Aggregate's accumulator can be any type you need, not just a running number.

Real-World Example

A sales dashboard needs a handful of summary statistics computed from the same underlying order data — exactly the kind of "headline numbers" every admin dashboard shows at the top of the page:

public record Order(int Id, int CustomerId, DateOnly OrderDate, decimal Total); List<Order> orders = [ new(101, 1, new(2026, 1, 5), 120.50m), new(102, 2, new(2026, 1, 18), 75.00m), new(103, 1, new(2026, 2, 2), 200.00m), new(104, 3, new(2026, 2, 14), 50.25m), new(105, 1, new(2026, 2, 20), 99.99m), new(106, 2, new(2026, 3, 1), 310.00m), ]; var dashboard = new { TotalOrders = orders.Count(), TotalRevenue = orders.Sum(o => o.Total), AverageOrder = orders.Average(o => o.Total), LargestOrder = orders.Max(o => o.Total), SmallestOrder = orders.Min(o => o.Total), BigSpenderOrders = orders.Count(o => o.Total > 100m) }; Console.WriteLine($""" Orders: {dashboard.TotalOrders} Revenue: ${dashboard.TotalRevenue} Avg order: ${dashboard.AverageOrder:F2} Range: ${dashboard.SmallestOrder} – ${dashboard.LargestOrder} Orders over $100: {dashboard.BigSpenderOrders} """); // Orders: 6 // Revenue: $855.74 // Avg order: $142.62 // Range: $50.25 – $310.00 // Orders over $100: 3

This pattern — several aggregation calls against the same source, each answering a different specific question — is exactly how most dashboard and report "headline numbers" are computed in real applications.

Analogy

A Calculator Tape, Not a Filing Cabinet

Every other LINQ operator in this module hands you back another drawer full of items — still a collection, just reshaped. Aggregation is different: it's like feeding a stack of receipts through an adding machine. What comes out the other end isn't a stack of receipts anymore — it's a single printed total on the tape. Aggregate is that adding machine with a custom program: you decide exactly how each receipt updates the running tape, not just "add it up."

Under the Hood

AGGREGATION IS EAGER, NOT DEFERRED
1. THERE'S NO SEQUENCE LEFT TO DEFER
2. Sum IS CONCEPTUALLY JUST Aggregate WITH A FIXED FUNCTION
// Conceptually, Sum(selector) is:
source.Aggregate(0m, (total, item) => total + selector(item));
3. AN EMPTY SEQUENCE BEHAVES DIFFERENTLY DEPENDING ON THE METHOD

Common Confusion

1. Count() the LINQ method vs a .Count property

Count() — with parentheses — is a LINQ extension method that, for a general IEnumerable<T>, may need to enumerate the whole sequence to count it. Many concrete collection types (List<T>, arrays via .Length) already track their count, and calling the property directly is cheaper than the LINQ method. This distinction matters enough to get its own deeper treatment in the LINQ Performance lesson at the end of this module.

2. Empty sequences don't fail the same way across every aggregate method

It's easy to assume all the aggregate methods behave consistently on an empty sequence. They don't: Sum and Count return 0; Average, Min, and Max throw. Always check Any() first (or use the safer overloads discussed in Common Mistakes) before calling Average, Min, or Max on a sequence that might be empty.

Common Mistakes

Mistake 1 — Calling Average/Min/Max on a sequence that might be empty

orders.Where(o => o.CustomerId == 999).Average(o => o.Total) throws InvalidOperationException the moment there are zero matching orders. Guard with Any() first, or reach for the nullable-returning overloads that exist for exactly this: orders.Where(...).Select(o => o.Total).DefaultIfEmpty().Average(), or simply check if (filtered.Any()) { ... } before aggregating.

Mistake 2 — Reaching for Aggregate when a specific method already exists

orders.Aggregate(0m, (sum, o) => sum + o.Total) works, but it's a longer, less self-documenting way to write orders.Sum(o => o.Total). Always prefer the specific, named method when your reduction maps directly onto count, sum, average, min, or max — reserve Aggregate for reductions those methods genuinely can't express.

When Should I Use It?

Use Count/Sum/Average/Min/Max when

Use Aggregate when

Mental Model

Count/Sum/Average/Min/Max = "the five most common ways to collapse a sequence into one number"
Aggregate = "the general-purpose fold — build your own reduction when none of the five fit"

Remember:
· Aggregation methods run immediately and eagerly — there's no sequence left to defer.
· Sum/Count return 0 for an empty sequence; Average/Min/Max throw.
· Always prefer a specific named method over Aggregate when one already fits.

Key Takeaway


Check Your Understanding

You've learned how to collapse sequences into summary values. Let's check your understanding.

1. What happens when you call emptyOrders.Average(o => o.Total) on a sequence with zero items?

Show answer

Correct: C

Why C is correct: As covered in Under the Hood and Common Confusion, Average (along with Min and Max) throws on an empty sequence because there's no mathematically meaningful average of zero items.

Why A is incorrect: Returning 0 for an empty sequence is the behavior of Sum and Count, not Average.

Why B is incorrect: The standard Average overload doesn't return a nullable type or silently produce null — it throws.

Why D is incorrect: LINQ operators don't block or wait for more items — an empty in-memory sequence is fully known and processed immediately, resulting in an exception.

Reinforcement: Always confirm a sequence is non-empty (with Any()) before calling Average, Min, or Max.

2. Why can't Sum() be deferred the way Where() is?

Show answer

Correct: B

Why B is correct: As explained in Under the Hood, deferred execution works because operators like Where return an object describing future work. Sum() has nothing like that to return — its return type is a plain number, which can only exist once the computation has actually finished.

Why A is incorrect: This is exactly the misconception the lesson corrects — aggregation methods are eager precisely because Where's deferral trick isn't available to them.

Why C is incorrect: This behavior doesn't depend on the concrete source type — it's inherent to what Sum() returns, regardless of whether the source is a List<T>, an array, or any other IEnumerable<T>.

Why D is incorrect: Deferred execution has nothing to do with the return type being bool — it's about whether the method can return a lazy, not-yet-evaluated sequence at all.

Reinforcement: Return type is the giveaway: IEnumerable<T> can be deferred; a scalar value cannot.

3. You need to reduce a sequence of temperature readings into both the highest reading and the number of times the temperature dropped compared to the previous reading — two pieces of running state that no single built-in aggregate method computes together. Which operator is the right fit?

Show answer

Correct: B

Why B is correct: This is exactly the shape of problem Aggregate is built for — as shown in the "largest jump" example, an accumulator can be any type, including a tuple carrying multiple pieces of running state through the whole sequence in a single pass.

Why A is incorrect: Sum only ever tracks a single running numeric total — it has no way to carry a second piece of state like a drop counter.

Why C is incorrect: Calling Max() twice would give you the highest reading, but nothing at all about how many times the temperature dropped between consecutive readings.

Why D is incorrect: This is precisely the situation Aggregate exists to avoid a manual loop for — it's a fully LINQ-native way to express custom, stateful accumulation.

Reinforcement: Whenever a reduction needs more than one running value, reach for Aggregate with a tuple or custom accumulator type.

4. Why is orders.Sum(o => o.Total) generally preferred over orders.Aggregate(0m, (sum, o) => sum + o.Total) even though both produce the same result?

Show answer

Correct: B

Why B is correct: As covered in Common Mistakes, reaching for Aggregate when a specific named method already exists is a real mistake — it produces the same answer with less clarity. Naming your intent directly (Sum) is preferred whenever it's available.

Why A is incorrect: Aggregate works perfectly well with decimals, as the example shows — it's a fully general tool, not restricted by type.

Why C is incorrect: Aggregate is a standard, actively supported part of LINQ — it's simply the wrong choice for this particular case, not deprecated.

Why D is incorrect: As shown in Under the Hood, Sum is conceptually just Aggregate with a fixed accumulator function — for this exact case, they are functionally equivalent.

Reinforcement: Reach for the specific method first; fall back to Aggregate only when no specific method expresses what you need.

You can now summarize sequences into meaningful numbers. Next up: the crucial idea underneath every LINQ operator you've used so far — deferred execution.


dotnetmadeeasy.com — Learn C# and .NET, the right way.