1. Intro
When working with collections, we often need not just to go through each element and do something, but to get a quick "summary" of the whole data set. For example:
- Count how many students got an A.
- Find out how many students are in the school.
- Calculate the total score all students got on the exam.
- Find the highest and lowest grades.
- Calculate the class average.
Sure, you could solve all these with a regular loop and a counter variable. But let's be real: nobody's excited to write twenty lines of code for something this basic.
LINQ gives you a set of aggregate functions — ready-to-use methods that take a collection, run through all the elements, and spit out an answer: sum, average, max, min, and sometimes even fancier stuff.
Here's a quick "aggregate table":
| Method | What it does | Returns |
|---|---|---|
|
Counts the number of elements | |
|
Sums up numeric values | Depends on element type (, , ...) |
|
Calculates the arithmetic mean | or another numeric type |
|
Finds the max element | Collection element |
|
Finds the min element | Collection element |
All these methods are extension methods for collections that implement IEnumerable<T>. More details — official LINQ aggregate methods docs.
2. Getting data ready for practice
Let's keep working with a simple student tracking app. Say we have this class:
// Student model
public class Student
{
public string Name { get; set; }
public int Grade { get; set; } // Grade on a five-point scale
public string Email { get; set; }
}
And a list:
// Basic student list
List<Student> students = new List<Student>
{
new Student { Name = "Ivan", Grade = 5, Email = "ivan@example.com" },
new Student { Name = "Olga", Grade = 4, Email = "olga@example.com" },
new Student { Name = "Artem", Grade = 3, Email = "artem@example.com" },
new Student { Name = "Darya", Grade = 5, Email = "darya@example.com" },
new Student { Name = "Petr", Grade = 2, Email = "petr@example.com" }
};
3. Counting elements: Count()
Basic stats
Say you want to know how many students you have in total:
int totalStudents = students.Count(); // 5
Console.WriteLine($"Total students: {totalStudents}");
LINQ magic: It's simple! The Count() method returns the number of elements in the collection.
Counting with a condition
How many straight-A students in the class?
int excellentStudents = students.Count(s => s.Grade == 5);
Console.WriteLine($"A-students: {excellentStudents}");
Here we pass a lambda to Count — it returns only those who match the condition (s.Grade == 5). Inside LINQ, this is the same as Where(...).Count(), but shorter and a bit more efficient.
What if the collection is empty?
If the collection is empty, Count honestly returns 0 — no error, no drama.
4. Summing values: Sum()
Getting the total of all grades
Say you want to know the total points scored by the class:
int sumOfGrades = students.Sum(s => s.Grade); // 5+4+3+5+2 = 19
Console.WriteLine($"Total grades: {sumOfGrades}");
Sum takes a selector (lambda expression) that returns a value for each element.
Summing a collection of numbers
If you just have a list of numbers, you don't need a selector:
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = numbers.Sum(); // 15
Common mistakes and quirks
If the collection is empty, Sum() for numeric types returns 0. But if the collection is made of nullable types (int?, double?), Sum still works fine: it ignores null values.
5. Arithmetic mean: Average()
Calculating the class average
Classic: what's the average grade per student?
double averageGrade = students.Average(s => s.Grade); // (5+4+3+5+2)/5 = 3.8
Console.WriteLine($"Average grade: {averageGrade:F2}");
Average — super useful for stats. Notice: the return value is always double, even if the original values were int. That way you don't lose the decimal part.
If there are no elements
If the original collection is empty, calling Average() throws an InvalidOperationException. This is a super common gotcha: if you're not sure the collection has stuff in it, check first!
if (students.Any())
Console.WriteLine(students.Average(s => s.Grade));
else
Console.WriteLine("No data to calculate average!");
Average value for a numeric array
double avg = numbers.Average();
6. Max and min: Max() and Min()
Who's the star and who's at the bottom
Want to know who's carrying the class with their grades, and who's... well, making the teacher nervous? Easy:
int maxGrade = students.Max(s => s.Grade); // 5
int minGrade = students.Min(s => s.Grade); // 2
Console.WriteLine($"Max grade: {maxGrade}");
Console.WriteLine($"Min grade: {minGrade}");
But who exactly is the hero?
Sometimes you want not just the number, but the name of the one who got it. Get the student object with the highest grade:
// Grab the first A-student after sorting descending
var bestStudent = students.OrderByDescending(s => s.Grade).First();
Console.WriteLine($"Best student: {bestStudent.Name} ({bestStudent.Grade})");
In newer .NET versions there's also MaxBy, which does this even cleaner. It's available since .NET 6, and .NET 9 added even more goodies (see MaxBy on Microsoft Docs). But even without MaxBy, you can totally use sorting and .First().
Traps and differences
If the collection is empty, calling Max() and Min() will also throw InvalidOperationException. So be ready for that (especially if you didn't check the collection first):
if (students.Any())
Console.WriteLine($"Max grade: {students.Max(s => s.Grade)}");
else
Console.WriteLine("No students to find max.");
7. Examples of "chaining" aggregate methods
Aggregate methods are often used together with filtering and projection:
// Average grade among A-students
double avgExcellent = students
.Where(s => s.Grade == 5)
.Average(s => s.Grade); // always 5, but it's a good example
// Total points among students with grade at least 4
int sumGood = students
.Where(s => s.Grade >= 4)
.Sum(s => s.Grade);
// Number of unique emails (just in case)
int uniqueEmails = students
.Select(s => s.Email)
.Distinct()
.Count();
This is where LINQ really shines: simple combos of operations let you write expressive, readable code that even your cat could understand (well, if your cat is a Junior C# Developer).
8. Comparing to the "manual" approach: why use aggregates?
Let's compare LINQ to regular loops using the average grade as an example:
Regular approach:
int sum = 0;
int count = 0;
foreach (var s in students)
{
sum += s.Grade;
count++;
}
double average = (count != 0) ? (double)sum / count : 0;
LINQ:
double average = students.Average(s => s.Grade);
Even adding empty collection handling — way easier!
9. Handy quirks
Quick note on performance and implementation details
LINQ aggregates, unlike most LINQ operations, run immediately! So when you call Sum(), Count(), Average(), Max(), Min(), the whole collection is actually processed right then. These methods return not a collection, but a single final result.
That's important: if you did something heavy before the aggregate, like a complex filter or transformation — it only runs once, at the moment you call the aggregate.
Do aggregate methods support query syntax?
Before running LINQ methods, newbies often ask: "Can I write all this in query syntax?" Short answer: query syntax itself doesn't have keywords for aggregates, but you can always mix it with method syntax:
var avg = (from s in students where s.Grade > 3 select s.Grade).Average();
Inside the parentheses — regular query syntax, then you call the aggregate method. People do this more often than you think!
10. Typical mistakes when using LINQ
Mistake #1: trying to use Average(), Max() or Min() on an empty collection.
If the collection is empty, Sum() and Count() will calmly return 0, but Average(), Max() and Min() will throw an exception. Before calling these, make sure the collection has at least one element.
Mistake #2: passing a lambda with the wrong signature.
For example, if you pass a string instead of a number to an aggregate function (Sum, Max, etc.), you'll get a compile error. It's especially easy to mess up with anonymous methods.
Mistake #3: inefficiently checking the number of elements.
The Where(...).Count() method first creates a new collection, then counts the elements. Instead, use Count(predicate) — it counts matching elements right away and works faster.
Mistake #4: ignoring nullable type quirks when aggregating.
If you're summing over int?, Sum() will ignore null values. That's correct behavior, but sometimes it can give you a surprise if you didn't expect it.
GO TO FULL VERSION