Skip to main content
ANVISoftware Solutions
Lesson 49 of 62Intermediate17 min

Aggregation

By the end of this lesson

Reduce a sequence to a single value with Count, Sum, Min, Max and Average.

The operators so far have all returned sequences. Aggregation operators return one value: a count, a total, a highest figure, a yes or no. They are also the point at which the query stops being a description and runs, because a single number cannot be produced without reading the items.

They are the easiest operators to use and they hide the sharpest edge in LINQ. Two of them throw an exception on an empty sequence and three do not, and which is which is not something you can guess.

The seven you will use most
C#
int headcount = employees.Count();                                 // 8
int engineers = employees.Count(e => e.Department == "Engineering"); // 3

decimal payroll = employees.Sum(e => e.AnnualSalary);              // 11830000
decimal lowest = employees.Min(e => e.AnnualSalary);               // 720000
decimal highest = employees.Max(e => e.AnnualSalary);              // 2700000
decimal average = employees.Average(e => e.AnnualSalary);          // 1478750

bool anyoneAboveTwoMillion = employees.Any(e => e.AnnualSalary > 2_000_000m);  // True
bool everyoneJoinedAfter2015 = employees.All(e => e.JoiningDate.Year > 2015);  // True

// Max gives you the number. MaxBy gives you the item that produced it.
Employee? topEarner = employees.MaxBy(e => e.AnnualSalary);
Console.WriteLine(topEarner?.Name);                                // Meera Nair
  • Count with no argument counts everything. Count with a predicate counts matches, so Count(p) saves a Where.
  • Sum, Min, Max and Average all take a lambda that picks the number to work on. They do not operate on employees, they operate on the values that lambda returns.
  • Any asks whether at least one item matches and stops at the first one it finds. All asks whether every item matches and stops at the first one that fails. Both return a bool, and neither reads more of the sequence than it has to.
  • MaxBy and MinBy return the item with the highest or lowest key, which is what you usually want for "the top earner" rather than "the top salary". They were added in .NET 6. On an empty sequence they do not give you an employee, so the result needs checking before use.
  • Each of these lines runs the query at that moment. Eight of them means eight walks over the list — negligible here, worth thinking about if the source is expensive to read.
Two ways to handle a sequence that might be empty
C#
List<Employee> legalTeam = employees
    .Where(e => e.Department == "Legal")
    .ToList();                                   // nobody works in Legal

Console.WriteLine(legalTeam.Count);              // 0
Console.WriteLine(legalTeam.Sum(e => e.AnnualSalary));   // 0

// Both of these throw InvalidOperationException:
// legalTeam.Average(e => e.AnnualSalary);
// legalTeam.Max(e => e.AnnualSalary);

// Option 1 — ask before you aggregate.
if (legalTeam.Any())
{
    Console.WriteLine(legalTeam.Average(e => e.AnnualSalary));
}
else
{
    Console.WriteLine("No salary data for that department.");
}

// Option 2 — make "no answer" representable.
decimal? averageOrNull = legalTeam.Average(e => (decimal?)e.AnnualSalary);   // null
decimal? highestOrNull = legalTeam.Max(e => (decimal?)e.AnnualSalary);       // null

Console.WriteLine(averageOrNull?.ToString() ?? "not available");             // not available
  • Casting to decimal? inside the lambda selects a different overload: the nullable one, which returns null for an empty sequence instead of throwing. The same works for Min and Max.
  • Option 1 is clearer when the empty case needs its own message or its own branch. Option 2 is better when the value travels onwards — a nullable carries "no data" all the way to the screen, where a 0 would be indistinguishable from a real zero.
  • Which to choose is a design decision about what the reader of the report should see: a dash, a zero, or nothing at all. The language will not make it for you; it will only throw if you avoid making it.
  • Note the difference between legalTeam.Count, which is the List property, and legalTeam.Count(), which is the LINQ method. On a List both give the same answer, and the property is the direct one.

Any() and Count() > 0 answer the same question with different amounts of work:

 employees.Any(p)employees.Count(p) > 0
What it doesStops as soon as one item matchesTests every item, counts the matches, then compares
On a large filtered sequenceMay examine one itemExamines all of them
On a plain List with no predicateCheapAlso cheap — the list already knows its own count
On an expensive source (a file, a network call, a computed chain)Reads the minimum neededReads everything
With Entity Framework CoreBecomes an existence check in SQLBecomes a count, which the database must complete first
States the questionIs there any?How many, and is that above zero?

Summary

  • Count, Sum, Min, Max and Average reduce a sequence to one value and run the query immediately
  • Min, Max and Average throw InvalidOperationException on an empty sequence; Count and Sum return 0
  • Casting the selected value to a nullable type makes "no data" a value instead of an exception
  • Any stops at the first match, so it avoids work that Count() > 0 has to do on any computed sequence
  • All returns true for an empty sequence, and Sum over int can overflow

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Try it yourself

Write a method that takes the employees list and a department name and prints its headcount, total salary and average salary.

It must produce sensible output for "Support" and for "Legal", where nobody works, without throwing.

Show solution

Materialising the filtered sequence once with ToList is deliberate. Four aggregate calls over the same query would each re-run the filter, and reading the list once makes the early return on empty straightforward.

The guard clause is the part that matters. Without it the method works perfectly for every department that has staff, which is every department anybody tests, and throws InvalidOperationException the first time a real user picks an empty one.

The nullable projection from option 2 in the lesson would work equally well here, and would suit a method that returns values rather than printing them. Both are correct; the choice depends on whether the caller needs to distinguish "no data" from a genuine zero.

C#
public static void PrintDepartmentSummary(List<Employee> employees, string department)
{
    List<Employee> members = employees
        .Where(e => e.Department == department)
        .ToList();

    if (members.Count == 0)
    {
        Console.WriteLine($"{department}: no employees recorded.");
        return;
    }

    Console.WriteLine(
        $"{department}: {members.Count} people, " +
        $"total {members.Sum(e => e.AnnualSalary)}, " +
        $"average {members.Average(e => e.AnnualSalary)}");
}

PrintDepartmentSummary(employees, "Support");
// Support: 2 people, total 1680000, average 840000

PrintDepartmentSummary(employees, "Legal");
// Legal: no employees recorded.

Think about it

Think about it

A colleague replaces every if (items.Count() > 0) in a codebase with if (items.Any()), citing performance.

Where does that change genuinely save work, and where is it only a change of wording?

Show solution

It saves real work whenever producing the count requires producing every item: a Where chain, a Select over a large list, lines read from a file, or a database query where a count has to be completed before an answer comes back. Any stops at the first item; Count has to finish.

It changes nothing measurable when the source already knows its size. A List, an array or a Dictionary exposes a count directly, and LINQ's Count() takes that shortcut, so both are a single field read. Claiming a speed-up there is not accurate.

The better argument for Any is that it states the question. "Is there at least one?" is what the if is asking, and Count() > 0 makes a reader confirm that the comparison is with zero and not with one. Readability holds everywhere; the performance claim holds only for sequences that have to be computed — which is an honest reason to prefer it as a habit, as long as the reason given is the right one.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

No employee works in Legal. What happens when you run employees.Where(e => e.Department == "Legal").Average(e => e.AnnualSalary)?

Saved in this browser only.