1. Introduction
If you've ever written a "classic" LINQ query with grouping, like:
var cityCounts = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Count = g.Count() });
then you probably noticed that for a simple count by group, the code looks kinda wordy. With the release of .NET 9, the Microsoft team decided to make devs' lives easier and added two super popular methods to LINQ:
- CountBy — a quick way to count elements by key.
- AggregateBy — a universal aggregator by key (not just counting, but summing, and so on).
By the way, these methods showed up because the community asked for them a lot, and their analogs have been around in many "advanced" LINQ libraries for ages, like MoreLINQ.
2. The CountBy Method: Concise Group Counting
Description
CountBy is literally "grouping, then immediately counting." The method itself returns a collection of "key-count" pairs, which is super common in real-world data analysis: top cities, number of students by grades, frequency of anything.
Signature (simplified):
IEnumerable<(TKey Key, int Count)> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
How to Use It
Example 1: Number of Students by City
Let's say we have this class:
class Student
{
public string Name { get; set; }
public int Grade { get; set; }
public string City { get; set; }
}
And in our app — a list of students:
var students = new List<Student>
{
new Student { Name = "Ivan", Grade = 5, City = "Neonville" },
new Student { Name = "Anna", Grade = 4, City = "Los Santos" },
new Student { Name = "Egor", Grade = 3, City = "Neonville" },
new Student { Name = "Maria", Grade = 5, City = "Rosewater" },
new Student { Name = "Oleg", Grade = 4, City = "Neonville" }
};
In .NET 8 and earlier, you'd have to write:
var group = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Count = g.Count() });
foreach (var cityInfo in group)
{
Console.WriteLine($"{cityInfo.City}: {cityInfo.Count} students");
}
In .NET 9, this is all done with one concise method:
var cityCounts = students.CountBy(s => s.City);
foreach (var (city, count) in cityCounts)
{
Console.WriteLine($"{city}: {count} students");
}
Yep, you see it right — no manual GroupBy, no Count() calculation — it's just simple and direct!
Example 2: Counting by Grades
A classic task for any dean — find out how many straight-A students and C students are in the group:
var gradeCounts = students.CountBy(s => s.Grade);
foreach (var (grade, count) in gradeCounts)
{
Console.WriteLine($"Grade {grade}: {count} students");
}
Example 3: Character Frequency in a String
CountBy works not just with objects, but with simpler stuff too:
string word = "supercalifragilisticexpialidocious";
var charFrequencies = word.CountBy(ch => ch);
foreach (var (letter, count) in charFrequencies)
{
Console.WriteLine($"{letter} : {count}");
}
How It Looks in Memory (Illustration)
| Key | Count (before CountBy) | Count (with CountBy) |
|---|---|---|
|
3 | 3 |
|
1 | 1 |
|
1 | 1 |
This approach makes your code readable and cuts down on bugs when grouping.
Common Mistakes and Gotchas
Don't forget, CountBy returns a tuple (Key, Count), not an anonymous type. Sometimes that's a bit less semantic, but it's super clear. Also, you can't directly specify what number type you want for Count — it's always int.
3. The AggregateBy Method: Universal Group Aggregates
Description
If CountBy is just a simple count by key, then AggregateBy is a Swiss Army knife! Want to sum sales by seller, get the max grade by class, or basically anything you can do with an accumulator? AggregateBy does it all.
Signature (super simplified):
IEnumerable<(TKey Key, TAccumulate Result)> AggregateBy<TSource, TKey, TAccumulate>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> func)
- keySelector — what to group by.
- seed — starting value for accumulation.
- func — function describing how to "build up" the result.
How to Use It
Example 1: Sum of Grades by City
Instead of this monster LINQ:
var sums = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Sum = g.Sum(s => s.Grade) });
You write:
var sums = students.AggregateBy(
s => s.City, // group by city
0, // starting sum
(acc, s) => acc + s.Grade // add grade
);
foreach (var (city, sum) in sums)
{
Console.WriteLine($"{city}: sum of grades = {sum}");
}
Example 2: Max Grade by City
Want the max value? Just change the accumulator:
var maxByCity = students.AggregateBy(
s => s.City,
int.MinValue, // starting value — as low as possible
(max, s) => Math.Max(max, s.Grade)
);
foreach (var (city, maxGrade) in maxByCity)
{
Console.WriteLine($"{city}: max grade = {maxGrade}");
}
Example 3: Collecting Names into a String
Let's update our student list:
var namesByCity = students.AggregateBy(
s => s.City,
"", // starting string
(acc, s) => string.IsNullOrEmpty(acc) ? s.Name : acc + ", " + s.Name
);
foreach (var (city, names) in namesByCity)
{
Console.WriteLine($"{city}: {names}");
}
Under the Hood (Diagram)
. {Student, Student, Student, ...}
|
| (group by city)
v
["Neonville"] ["Rosewater"] ["Los Santos"]
| | |
| (accumulation) |
|___________________________|
|
v
{("Neonville", sum), ("Rosewater", sum), ...}
Comparison with "Old" Ways
Before, for all these tasks, you had to write GroupBy + Select, and inside that, your own Sum, Aggregate, or some other trick. Now AggregateBy hides all that "pain" and makes your code short and expressive.
Practice: Using It in an App
Our student project — a student diary. Let's add a report for average grade by city.
// Calculate average grade with AggregateBy — accumulate sum and count, then get the average
var avgByCity = students.AggregateBy(
s => s.City,
(Sum: 0, Count: 0),
(acc, s) => (acc.Sum + s.Grade, acc.Count + 1)
).Select(x => (x.Key, Average: (double)x.Result.Sum / x.Result.Count));
foreach (var (city, avg) in avgByCity)
{
Console.WriteLine($"{city}: average grade = {avg:F2}");
}
Here we accumulate sum and count in a tuple, then divide sum by count to get the average.
Features and Pitfalls
If your accumulator is a reference type, don't forget: inside AggregateBy the same reference is used for all iterations in the group, so don't mutate the object, or you might accidentally mess up the whole group's result.
Also: if you need not just grouping, but also further transformation of results — you can chain .Select right away. Try to pick a semantically correct seed (starting value), so you don't get weird results (like, don't use 0 for strings).
4. Comparing Old and New Approaches
| Task | Old LINQ | New LINQ .NET 9 |
|---|---|---|
| Number of students by city | |
|
| Sum of grades by city | |
|
| Max grade by city | |
|
| Character frequency in a string | |
|
Practical Value for Work and Real Projects
Methods like CountBy and AggregateBy are must-haves when writing statistical reports, prepping data for visualization, building pivot tables, and other typical tasks. They cut down your code, make it more readable, and most importantly, more reliable: less manual "debugging" with grouping and aggregation, less chance to mess up the logic.
At a Junior/Middle interview, knowing the new LINQ methods shows you keep up with the platform, and in production — it lets you skip reinventing the wheel and use elegant, clear constructs that are easy to read and maintain.
GO TO FULL VERSION