.NET LINQ Aggregation
Introduction
LINQ (Language Integrated Query) aggregation operations allow you to perform calculations on sequences of values, producing a single result. These operations are essential for data analysis tasks when working with collections in C#. Whether you need to calculate totals, find maximum values, or determine averages, LINQ aggregation methods make these tasks straightforward and elegant.
In this tutorial, you'll learn:
- What LINQ aggregation methods are and when to use them
- How to use common aggregation operators like
Sum
,Average
,Count
,Min
, andMax
- How to work with more advanced aggregation techniques
- How to handle edge cases and avoid common pitfalls
Understanding LINQ Aggregation Methods
Aggregation methods in LINQ process a collection of values and return a single value. Think of them as functions that condense an entire sequence down to a single result. These methods are part of the LINQ standard query operators and are available for all collections that implement IEnumerable<T>
.
Common LINQ Aggregation Methods
Count and LongCount
The Count
method returns the number of elements in a sequence, optionally filtered by a condition.
// Basic counting
List<string> fruits = new List<string> { "Apple", "Banana", "Cherry", "Date", "Elderberry" };
int totalFruits = fruits.Count();
Console.WriteLine($"Total fruits: {totalFruits}"); // Output: Total fruits: 5
// Conditional counting
int shortNamedFruits = fruits.Count(fruit => fruit.Length <= 5);
Console.WriteLine($"Fruits with names of 5 or fewer letters: {shortNamedFruits}"); // Output: Fruits with names of 5 or fewer letters: 3
Use LongCount
instead of Count
when you expect the count to potentially exceed the maximum value of int
(which is very large collections).
Sum
The Sum
method calculates the total of numeric values in a sequence.
// Summing integers
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int sum = numbers.Sum();
Console.WriteLine($"Sum: {sum}"); // Output: Sum: 15
// Summing properties of objects
List<Product> products = new List<Product>
{
new Product { Name = "Laptop", Price = 1200 },
new Product { Name = "Phone", Price = 800 },
new Product { Name = "Tablet", Price = 400 }
};
decimal totalValue = products.Sum(p => p.Price);
Console.WriteLine($"Total product value: {totalValue}"); // Output: Total product value: 2400
Average
The Average
method calculates the arithmetic mean of numeric values in a sequence.
// Average of integers
List<int> scores = new List<int> { 85, 92, 78, 95, 88 };
double averageScore = scores.Average();
Console.WriteLine($"Average score: {averageScore}"); // Output: Average score: 87.6
// Average of properties
List<Student> students = new List<Student>
{
new Student { Name = "Alice", Grade = 93 },
new Student { Name = "Bob", Grade = 85 },
new Student { Name = "Charlie", Grade = 77 }
};
double classAverage = students.Average(s => s.Grade);
Console.WriteLine($"Class average: {classAverage}"); // Output: Class average: 85
Min and Max
These methods find the minimum and maximum values in a sequence.
// Min and Max of integers
List<int> temperatures = new List<int> { 72, 81, 65, 89, 75 };
int lowestTemp = temperatures.Min();
int highestTemp = temperatures.Max();
Console.WriteLine($"Temperature range: {lowestTemp} to {highestTemp}"); // Output: Temperature range: 65 to 89
// Min and Max of properties
List<Product> products = new List<Product>
{
new Product { Name = "Laptop", Price = 1200 },
new Product { Name = "Phone", Price = 800 },
new Product { Name = "Tablet", Price = 400 }
};
decimal cheapestPrice = products.Min(p => p.Price);
decimal mostExpensivePrice = products.Max(p => p.Price);
Console.WriteLine($"Price range: {cheapestPrice} to {mostExpensivePrice}"); // Output: Price range: 400 to 1200
// Finding an object with the maximum value
Product mostExpensiveProduct = products.MaxBy(p => p.Price); // Requires .NET 6 or later
Console.WriteLine($"Most expensive product: {mostExpensiveProduct.Name}"); // Output: Most expensive product: Laptop
Aggregate
The Aggregate
method is the most flexible aggregation operator, allowing you to apply a custom accumulation function to a sequence.
// Simple aggregation: multiplying all numbers together
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int product = numbers.Aggregate((acc, curr) => acc * curr);
Console.WriteLine($"Product of all numbers: {product}"); // Output: Product of all numbers: 120
// Aggregation with a seed value
int sumPlusTen = numbers.Aggregate(10, (acc, curr) => acc + curr);
Console.WriteLine($"Sum plus ten: {sumPlusTen}"); // Output: Sum plus ten: 25
// More complex aggregation: building a comma-separated string
string fruitList = fruits.Aggregate((acc, curr) => acc + ", " + curr);
Console.WriteLine($"Fruits: {fruitList}"); // Output: Fruits: Apple, Banana, Cherry, Date, Elderberry
// Aggregation with seed and result selector
string joinedWithPrefix = fruits.Aggregate(
"Fruits: ", // seed
(acc, curr) => acc + curr + ", ", // accumulator function
result => result.TrimEnd(',', ' ') // result selector
);
Console.WriteLine(joinedWithPrefix); // Output: Fruits: Apple, Banana, Cherry, Date, Elderberry
Handling Edge Cases
When working with aggregation methods, it's important to handle empty collections and null values properly.
Empty Collections
Most LINQ aggregate methods will throw an InvalidOperationException
when called on empty sequences:
List<int> emptyList = new List<int>();
try
{
// This will throw an exception
int average = emptyList.Average();
}
catch (InvalidOperationException ex)
{
Console.WriteLine("Cannot calculate average of empty collection");
}
// Safe alternatives:
int count = emptyList.Count(); // Returns 0, doesn't throw
int? sum = emptyList.Sum(); // Returns 0, doesn't throw
double? safeAverage = emptyList.DefaultIfEmpty().Average(); // Returns 0
int? min = emptyList.DefaultIfEmpty().Min(); // Returns default value (0)
Nullable Types
When working with nullable types, use the appropriate overloads:
List<int?> nullableNumbers = new List<int?> { 1, 2, null, 4, 5 };
// These calculate correctly, ignoring null values
double average = nullableNumbers.Average(n => n ?? 0); // Treats nulls as zeros
int sum = nullableNumbers.Sum(n => n ?? 0); // Treats nulls as zeros
// This counts only non-null values
int nonNullCount = nullableNumbers.Count(n => n.HasValue);
Console.WriteLine($"Non-null count: {nonNullCount}"); // Output: Non-null count: 4
Practical Examples
Example 1: Sales Analysis
public class SalesRecord
{
public string Product { get; set; }
public string Category { get; set; }
public decimal Amount { get; set; }
public DateTime Date { get; set; }
}
// Sample sales data
List<SalesRecord> sales = new List<SalesRecord>
{
new SalesRecord { Product = "Laptop", Category = "Electronics", Amount = 1200, Date = new DateTime(2023, 1, 15) },
new SalesRecord { Product = "Monitor", Category = "Electronics", Amount = 400, Date = new DateTime(2023, 1, 20) },
new SalesRecord { Product = "Desk", Category = "Furniture", Amount = 350, Date = new DateTime(2023, 2, 5) },
new SalesRecord { Product = "Chair", Category = "Furniture", Amount = 250, Date = new DateTime(2023, 2, 5) },
new SalesRecord { Product = "Phone", Category = "Electronics", Amount = 800, Date = new DateTime(2023, 2, 10) }
};
// Total sales
decimal totalSales = sales.Sum(s => s.Amount);
Console.WriteLine($"Total sales: ${totalSales}"); // Output: Total sales: $3000
// Average sale amount
decimal averageSale = sales.Average(s => s.Amount);
Console.WriteLine($"Average sale: ${averageSale}"); // Output: Average sale: $600
// Most expensive product
var mostExpensiveProduct = sales.MaxBy(s => s.Amount);
Console.WriteLine($"Most expensive product: {mostExpensiveProduct.Product} (${mostExpensiveProduct.Amount})");
// Output: Most expensive product: Laptop ($1200)
// Sales by category
var salesByCategory = sales
.GroupBy(s => s.Category)
.Select(g => new { Category = g.Key, Total = g.Sum(s => s.Amount) });
foreach (var category in salesByCategory)
{
Console.WriteLine($"{category.Category}: ${category.Total}");
}
// Output:
// Electronics: $2400
// Furniture: $600
// Monthly sales
var monthlySales = sales
.GroupBy(s => new { s.Date.Year, s.Date.Month })
.Select(g => new {
Year = g.Key.Year,
Month = g.Key.Month,
Total = g.Sum(s => s.Amount)
})
.OrderBy(x => x.Year).ThenBy(x => x.Month);
foreach (var month in monthlySales)
{
Console.WriteLine($"{month.Year}-{month.Month}: ${month.Total}");
}
// Output:
// 2023-1: $1600
// 2023-2: $1400
Example 2: Student Grade Statistics
public class Student
{
public string Name { get; set; }
public string Subject { get; set; }
public int Grade { get; set; }
}
// Sample student data
List<Student> students = new List<Student>
{
new Student { Name = "Alice", Subject = "Math", Grade = 92 },
new Student { Name = "Alice", Subject = "Science", Grade = 88 },
new Student { Name = "Bob", Subject = "Math", Grade = 78 },
new Student { Name = "Bob", Subject = "Science", Grade = 85 },
new Student { Name = "Charlie", Subject = "Math", Grade = 95 },
new Student { Name = "Charlie", Subject = "Science", Grade = 90 }
};
// Overall class average
double classAverage = students.Average(s => s.Grade);
Console.WriteLine($"Class average across all subjects: {classAverage:F1}"); // Output: Class average across all subjects: 88.0
// Subject averages
var subjectAverages = students
.GroupBy(s => s.Subject)
.Select(g => new { Subject = g.Key, Average = g.Average(s => s.Grade) });
foreach (var subject in subjectAverages)
{
Console.WriteLine($"{subject.Subject} average: {subject.Average:F1}");
}
// Output:
// Math average: 88.3
// Science average: 87.7
// Student averages
var studentAverages = students
.GroupBy(s => s.Name)
.Select(g => new {
Student = g.Key,
Average = g.Average(s => s.Grade),
Highest = g.Max(s => s.Grade),
Lowest = g.Min(s => s.Grade)
});
foreach (var student in studentAverages)
{
Console.WriteLine($"{student.Student}: Avg = {student.Average:F1}, High = {student.Highest}, Low = {student.Lowest}");
}
// Output:
// Alice: Avg = 90.0, High = 92, Low = 88
// Bob: Avg = 81.5, High = 85, Low = 78
// Charlie: Avg = 92.5, High = 95, Low = 90
// Highest overall grade
var topStudent = students.MaxBy(s => s.Grade);
Console.WriteLine($"Highest grade: {topStudent.Name} in {topStudent.Subject} ({topStudent.Grade})");
// Output: Highest grade: Charlie in Math (95)
Advanced Aggregation Techniques
Combining Multiple Aggregations
// Calculate multiple statistics in one pass
var stats = numbers.Aggregate(
new { Count = 0, Sum = 0, Min = int.MaxValue, Max = int.MinValue },
(acc, num) => new {
Count = acc.Count + 1,
Sum = acc.Sum + num,
Min = Math.Min(acc.Min, num),
Max = Math.Max(acc.Max, num)
},
result => new {
Count = result.Count,
Sum = result.Sum,
Min = result.Min,
Max = result.Max,
Average = (double)result.Sum / result.Count
}
);
Console.WriteLine($"Stats - Count: {stats.Count}, Sum: {stats.Sum}, Min: {stats.Min}, Max: {stats.Max}, Avg: {stats.Average}");
// Output: Stats - Count: 5, Sum: 15, Min: 1, Max: 5, Avg: 3
Working with Custom Comparisons
public class Person
{
public string Name { get; set; }
public DateTime Birthday { get; set; }
}
List<Person> people = new List<Person>
{
new Person { Name = "Alice", Birthday = new DateTime(1990, 5, 15) },
new Person { Name = "Bob", Birthday = new DateTime(1985, 10, 10) },
new Person { Name = "Charlie", Birthday = new DateTime(1995, 3, 25) }
};
// Finding the oldest and youngest person
Person oldest = people.MinBy(p => p.Birthday);
Person youngest = people.MaxBy(p => p.Birthday);
Console.WriteLine($"Oldest person: {oldest.Name} (Born: {oldest.Birthday:d})");
Console.WriteLine($"Youngest person: {youngest.Name} (Born: {youngest.Birthday:d})");
// Output:
// Oldest person: Bob (Born: 10/10/1985)
// Youngest person: Charlie (Born: 3/25/1995)
Summary
LINQ aggregation methods provide powerful tools for calculating values from collections in C#. These methods can dramatically simplify your code when performing data analysis tasks:
Count
andLongCount
: Count elements in a sequenceSum
: Calculate the total of numeric valuesAverage
: Find the arithmetic meanMin
andMax
: Find minimum and maximum valuesMinBy
andMaxBy
: Find objects with minimum/maximum values of a propertyAggregate
: Apply custom accumulation functions
Remember to handle edge cases like empty collections and nullable types. LINQ aggregation methods often work best when combined with other LINQ operators like GroupBy
and Select
to create comprehensive data analysis solutions.
Exercises
-
Create a collection of products with names, categories, and prices, then calculate the total value of products in each category.
-
Use the
Aggregate
method to find both the longest and shortest string in a list of strings in a single pass. -
Given a collection of employee objects with departments and salaries, find the department with the highest average salary.
-
Create a report showing for each month of the year, the count, total, average, minimum and maximum values of a collection of financial transactions.
-
Implement a custom aggregation that calculates the median value of a collection of numbers.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)