Counting and Aggregating

Count, Sum, Min, Max, Average and Aggregate fold a sequence into one value; Any and All answer yes or no and stop early.

SyntaxTAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func)

An aggregate turns a whole sequence into a single value. LINQ gives you five ready-made ones, a general-purpose one you supply the step for, and two that answer a yes-or-no question.

The ready-made five

Count returns how many elements there are. Sum, Min, Max and Average do the obvious arithmetic. Each takes an optional lambda that picks the number out of each element, so orders.Sum(o => o.Total) adds up a field. Average over a sequence of int returns a double, because the mean of whole numbers usually is not one. Sum over int stays int and throws OverflowException if the total passes int.MaxValue; project to long first if that is possible. MinBy and MaxBy, added in .NET 6, return the element that owns the smallest or largest key rather than the key itself.

Any and All

Any() with no argument asks whether the sequence has at least one element. Any(predicate) asks whether at least one element passes, and stops at the first that does. All(predicate) asks whether every element passes, and stops at the first that fails. Both are cheaper than counting, so prefer list.Any() over list.Count() > 0.

Aggregate

Aggregate is the general case: you give it a starting value, called the seed, and a lambda that takes the running value and the next element and returns the new running value. It is the foreach loop with an accumulator, written as one expression.

Count(p) is shorthand for Where(p).Count(), and Sum(s) for Select(s).Sum(). Same results either way.

Example

Example · csharp
int[] sales = [12, 30, 7, 30, 21];

Console.WriteLine(sales.Count());                // how many elements
Console.WriteLine(sales.Count(n => n >= 21));    // how many pass the test
Console.WriteLine(sales.Sum());
Console.WriteLine(sales.Min());
Console.WriteLine(sales.Max());
Console.WriteLine(sales.Average());              // int input, double result
Console.WriteLine(sales.Any(n => n > 25));       // stops at the first match
Console.WriteLine(sales.All(n => n > 5));        // stops at the first failure

// Aggregate folds the sequence into one value using a step you write. The
// first argument is the seed, the starting value. The lambda receives the
// running value and the next element, and returns the new running value.
int product = sales.Aggregate(1, (running, next) => running * next);
Console.WriteLine(product);

// Output:
// 5
// 3
// 100
// 7
// 30
// 20
// True
// True
// 1587600

When to use it

  • Total the line items on an invoice and show the average order value on a dashboard.
  • Check whether a user holds any of the required roles, without counting them all.
  • Find the single slowest request in a batch of timing records, along with its URL.

More examples

Aggregate with a result selector, next to the loop it replaces

Seeing both side by side is the point. Aggregate earns its place inside a longer query chain, where breaking out to a loop would mean materialising an intermediate list. Standing alone, as here, the loop is easier to read and most reviewers will prefer it. There is also a two-argument Aggregate with no seed, which uses the first element as the starting value and throws InvalidOperationException on an empty sequence.

Example · csharp
string[] words = ["red", "amber", "green"];

// The three-argument Aggregate adds a final lambda that shapes the
// accumulated value into the answer you actually want.
string summary = words.Aggregate(
    0,                                      // seed: the starting total
    (total, word) => total + word.Length,   // one step per element
    total => $"{total} characters");         // shape the final value

// The same work written out as the loop Aggregate stands in for. Identical
// result; the loop is often the clearer of the two.
int running = 0;
foreach (string word in words)
{
    running += word.Length;
}
string byHand = $"{running} characters";

Console.WriteLine(summary);
Console.WriteLine(byHand);

// Output:
// 13 characters
// 13 characters

MaxBy, the Count property, and empty sequences

Use MaxBy whenever you need the whole winning record, not just its score; before .NET 6 that meant OrderByDescending followed by First, which sorted the entire sequence to find one element. The empty-sequence line is worth memorising: All returning true for an empty sequence surprises people, but it follows from the question being asked, since there is no element that fails.

Example · csharp
Reading[] readings =
[
    new Reading("north", 4),
    new Reading("south", 11),
    new Reading("east", 9)
];

// Max returns the largest key. MaxBy returns the element that has it. Both
// MinBy and MaxBy were added in .NET 6, and both return TSource?: on an
// empty sequence they give back null when TSource is a nullable type, and
// throw InvalidOperationException when TSource is a non-nullable struct.
int highest = readings.Max(r => r.Value);
Reading? hottest = readings.MaxBy(r => r.Value);

// On an array or a List<T>, prefer the Length or Count property over the
// Count() method: the property is already known, the method has to ask.
Console.WriteLine($"{highest} {hottest?.Site} {readings.Length}");

// Empty sequences: Sum is 0, Any is false, and All is true because no
// element failed the test.
int[] empty = [];
Console.WriteLine($"{empty.Sum()} {empty.Any()} {empty.All(n => n > 0)}");

// Output:
// 11 south 3
// 0 False True

record Reading(string Site, int Value);

Discussion

  • Be the first to comment on this lesson.