1. Einführung
Falls du schon mal einen "klassischen" LINQ-Query mit Gruppierung geschrieben hast, zum Beispiel:
var cityCounts = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Count = g.Count() });
dann hast du bestimmt gemerkt, dass der Code für einfaches Zählen pro Gruppe ziemlich umständlich aussieht. Mit .NET 9 hat das Microsoft-Team das Leben der Entwickler leichter gemacht und zwei oft gewünschte Methoden zu LINQ hinzugefügt:
- CountBy — ein schneller Weg, um die Anzahl der Elemente pro Schlüssel zu zählen.
- AggregateBy — ein universeller Aggregator pro Schlüssel (zählt nicht nur, sondern summiert, usw.).
Übrigens, diese Methoden sind auf Wunsch der Community entstanden, und ähnliche Features gibt es schon lange in vielen "fortgeschrittenen" LINQ-Bibliotheken wie MoreLINQ.
2. Die Methode CountBy: Kurz und knackig gruppieren
Beschreibung
CountBy ist im Grunde "Gruppieren und direkt danach zählen". Die Methode gibt eine Collection von "Schlüssel-Anzahl"-Paaren zurück, was in echten Datenanalyse-Aufgaben super oft gebraucht wird: Top-Städte, Anzahl der Studenten pro Note, Häufigkeit von irgendwas.
Signatur (vereinfacht):
IEnumerable<(TKey Key, int Count)> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
Praxisbeispiele
Beispiel 1: Anzahl der Studenten pro Stadt
Nehmen wir mal so eine Klasse:
class Student
{
public string Name { get; set; }
public int Grade { get; set; }
public string City { get; set; }
}
Und in unserer App — eine Liste von Studenten:
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 und früher musste man sowas schreiben:
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} Studenten");
}
In .NET 9 macht das ein einziger, kurzer Aufruf:
var cityCounts = students.CountBy(s => s.City);
foreach (var (city, count) in cityCounts)
{
Console.WriteLine($"{city}: {count} Studenten");
}
Ja, du siehst richtig — kein GroupBy mehr, kein Count() — alles easy und direkt!
Beispiel 2: Zählen nach Noten
Klassiker für jeden Dekan — wie viele Einser- und Dreier-Studenten gibt's in der Gruppe:
var gradeCounts = students.CountBy(s => s.Grade);
foreach (var (grade, count) in gradeCounts)
{
Console.WriteLine($"Note {grade}: {count} Studenten");
}
Beispiel 3: Buchstabenhäufigkeit in einem String
CountBy geht nicht nur mit Objekten, sondern auch mit einfacheren Sachen:
string word = "supercalifragilisticexpialidocious";
var charFrequencies = word.CountBy(ch => ch);
foreach (var (letter, count) in charFrequencies)
{
Console.WriteLine($"{letter} : {count}");
}
Wie sieht das im Speicher aus? (Illustration)
| Schlüssel | Count (vor CountBy) | Count (mit CountBy) |
|---|---|---|
|
3 | 3 |
|
1 | 1 |
|
1 | 1 |
So ein Ansatz macht den Code lesbar und reduziert Fehler beim Gruppieren.
Typische Fehler und Besonderheiten
Denk dran: CountBy gibt ein Tuple (Key, Count) zurück, keinen anonymen Typ. Das ist manchmal weniger sprechend, aber dafür maximal transparent. Außerdem kannst du nicht direkt den Zahlentyp für Count angeben — es kommt immer ein int zurück.
3. Die Methode AggregateBy: Universelle Aggregation pro Gruppe
Beschreibung
Wenn CountBy einfach nur zählt, ist AggregateBy das Schweizer Taschenmesser! Willst du die Verkaufssumme pro Verkäufer, die höchste Note pro Klasse oder irgendwas anderes, was man mit einem Akkumulator ausdrücken kann? Genau das macht AggregateBy.
Signatur (stark vereinfacht):
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 — nach welchem Kriterium gruppiert wird.
- seed — Startwert für die Akkumulation.
- func — Funktion, wie das Ergebnis "aufgebaut" wird.
Praxisbeispiele
Beispiel 1: Notensumme pro Stadt
Statt so einem LINQ-Monster:
var sums = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Sum = g.Sum(s => s.Grade) });
schreibst du einfach:
var sums = students.AggregateBy(
s => s.City, // gruppieren nach Stadt
0, // Startwert
(acc, s) => acc + s.Grade // Note addieren
);
foreach (var (city, sum) in sums)
{
Console.WriteLine($"{city}: Notensumme = {sum}");
}
Beispiel 2: Höchste Note pro Stadt
Willst du das Maximum — einfach den Akkumulator ändern:
var maxByCity = students.AggregateBy(
s => s.City,
int.MinValue, // Startwert — kleinstmöglich
(max, s) => Math.Max(max, s.Grade)
);
foreach (var (city, maxGrade) in maxByCity)
{
Console.WriteLine($"{city}: höchste Note = {maxGrade}");
}
Beispiel 3: Namen als String sammeln
Wir aktualisieren unsere Studentenliste:
var namesByCity = students.AggregateBy(
s => s.City,
"", // Startstring
(acc, s) => string.IsNullOrEmpty(acc) ? s.Name : acc + ", " + s.Name
);
foreach (var (city, names) in namesByCity)
{
Console.WriteLine($"{city}: {names}");
}
Illustration "von innen" (Schema)
. {Student, Student, Student, ...}
|
| (gruppieren nach city)
v
["Neonville"] ["Rosewater"] ["Los Santos"]
| | |
| (Akkumulation) |
|___________________________|
|
v
{("Neonville", summe), ("Rosewater", summe), ...}
Vergleich mit "alten" Methoden
Früher musste man für solche Aufgaben immer GroupBy + Select schreiben, darin dann Sum, Aggregate oder sonst was. Jetzt versteckt AggregateBy diesen "Schmerz" und macht den Code kurz und ausdrucksstark.
Praxis: Im eigenen Projekt nutzen
Unser Lernprojekt — ein Studententagebuch. Wir bauen einen Report für den Notendurchschnitt pro Stadt ein.
// Durchschnittsnote mit AggregateBy — wir akkumulieren Summe und Anzahl, dann berechnen wir den Schnitt
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}: Durchschnittsnote = {avg:F2}");
}
Hier akkumulieren wir Summe und Anzahl im Tuple und teilen dann Summe durch Anzahl für den Schnitt.
Besonderheiten und Stolperfallen
Wenn dein Akkumulator ein Referenztyp ist, denk dran: Innerhalb von AggregateBy wird für alle Iterationen pro Gruppe dieselbe Referenz verwendet, also nicht das Objekt mutieren, sonst beeinflusst du versehentlich das Ergebnis der ganzen Gruppe.
Und noch was: Wenn du nicht nur gruppieren, sondern auch das Ergebnis weiter transformieren willst — nutze direkt .Select. Wähle ein semantisch passendes seed (Startwert), damit du keine komischen Ergebnisse bekommst (z.B. kein 0 für Strings).
4. Vergleich: Alt vs. Neu
| Aufgabe | Alter LINQ | Neuer LINQ .NET 9 |
|---|---|---|
| Anzahl der Studenten pro Stadt | |
|
| Notensumme pro Stadt | |
|
| Höchste Note pro Stadt | |
|
| Buchstabenhäufigkeit im String | |
|
Praktischer Nutzen für Arbeit und echte Projekte
Solche Methoden wie CountBy und AggregateBy sind beim Schreiben von Statistik-Reports, Datenaufbereitung für Visualisierung, Pivot-Tabellen und anderen Standardaufgaben echt Gold wert. Sie machen den Code kürzer, lesbarer und — das Wichtigste — zuverlässiger: Weniger manuelles "Debugging" bei Gruppierung und Aggregation, weniger Chancen, sich in der Logik zu vertun.
Im Bewerbungsgespräch für Junior/Middle zeigt das Wissen über neue LINQ-Methoden, dass du am Puls der Plattform bist, und im echten Projekt sparst du dir eigene Workarounds und nutzt elegante, verständliche Konstrukte, die jeder easy lesen und pflegen kann.
GO TO FULL VERSION