Aggregations
Reducing a sequence to a single value — Count, Sum, Min, Max, Average, and the universal Aggregate
So far our operators have produced sequences. Aggregations are the operators that produce a single value. They're how you turn "a list of orders" into "the total revenue" or "the largest order".
This page covers the built-in shortcuts (Count, Sum, Min,
Max, Average) and then the general-purpose tool that underlies
all of them: Aggregate.
The friendly built-ins
| Operator | What it returns | Notes |
|---|---|---|
Count() | Number of elements | Walks the sequence; for ICollection<T> it's O(1) |
LongCount() | Same, as long | For very large counts |
Sum() | Sum of elements | Overloads for numeric types and a selector |
Min() | Smallest element | Throws on an empty non-nullable sequence |
Max() | Largest element | Same caveat |
Average() | Arithmetic mean | Returns double/decimal; throws on empty |
MinBy(key) | Element with smallest key | New in .NET 6 |
MaxBy(key) | Element with largest key | Same |
The lambda overloads (Sum(p => p.Price), Count(p => p.Stock > 0)) are the same as projecting first and then aggregating
— just a shortcut.
Empty-sequence behavior
Aggregations on empty sequences are a famous source of bugs.
| Operator | Empty input |
|---|---|
Count() | 0 |
Sum() | 0 (good!) |
Min(), Max(), Average() | Throws InvalidOperationException |
MinBy(), MaxBy() | Returns null |
First() | Throws |
FirstOrDefault() | Returns the default value |
The pattern is: if the operator has a meaningful answer for "no
elements" (count is 0, sum is 0), it returns that. If not, it
throws — and you can usually pick a …OrDefault variant if you
want a sentinel.
DefaultIfEmpty() yields one default element if the source is empty.
It's a useful trick for "give me the min, or zero".
The universal aggregator: Aggregate
Aggregate (sometimes called fold or reduce elsewhere) is the
general form. You give it:
- A seed — the starting accumulator value.
- An accumulator function —
(acc, element) => newAcc. - Optionally, a result selector that turns the final accumulator into the answer.
Sum, Count, Min, and Max are all special cases.
Aggregate is the swiss-army knife. Any time you need to fold
a sequence into a single value and there's no specialised operator
already, reach for Aggregate.
Aggregating into a richer accumulator
The accumulator doesn't have to be a simple type. It can be a tuple, record, or even a collection — useful when you want to compute several things in one pass.
One pass through nums, five quantities computed. Compared with five
separate Sum/Min/Max/Count/Sum calls (which would walk the source
five times), this is potentially much more efficient — though for
small in-memory collections the difference is negligible.
Aggregate is not always the best choice
Built-in operators are usually clearer and equally fast:
// Prefer this:
int sum = nums.Sum();
// Over this (works, but harder to read):
int sum = nums.Aggregate(0, (acc, n) => acc + n);Reach for Aggregate when:
- There is no built-in operator for what you want.
- You need to compute several things in one pass.
- The accumulator has a non-trivial type or update logic.
A walkthrough of one fold
Let's trace nums.Aggregate(0, (acc, n) => acc + n) for
nums = [3, 1, 4]:
The accumulator threads through, one element at a time. The seed is the starting point; the final accumulator is the result.
A multi-file challenge
Implement Checksum.Compute(IEnumerable<int> xs) which
returns a single int defined as:
Start at 0. For each element, multiply the accumulator by 31, then add the element. Return the accumulator.
That formula is a classic polynomial-rolling hash. Implement it
with Aggregate — no foreach allowed.
What does new int[0].Min() do?
Returns 0.
Returns int.MinValue.
Returns null.
Throws InvalidOperationException because the sequence is empty.