Wie gruppiert man Daten nach zwei oder mehr Feldern mit LINQ in C#?
\n\nHallo, Freund! Wenn du jemals Datenbankabfragen in SQL geschrieben hast, erinnerst du dich sicher an die GROUP BY-Klausel mit mehreren Spalten. Sie ermöglicht es, Daten in logische Körbe aufzuteilen – zum Beispiel Verkäufe zuerst nach Stadt und dann innerhalb der Stadt nach Produktkategorie zu gruppieren. In C# wird dafür GroupBy aus LINQ verwendet. Aber hier ist der Haken: Das Standard-GroupBy akzeptiert nur einen einzigen Schlüssel. Was tun, wenn man nach mehreren Spalten gruppieren muss?
Keine Sorge! Die Lösung ist elegant und einfach – wir erstellen einen anonymen Typ, der die benötigten Felder zu einem zusammengesetzten Schlüssel vereint. In diesem Artikel zeige ich dir nicht nur die grundlegende Syntax, sondern auch reale Codebeispiele, die du sofort in deinen Projekten anwenden kannst. Los geht's!
\n\nDas Problem: Ein Schlüssel ist zu wenig
\n\nStell dir vor, du hast eine Liste von Studenten:
\n\npublic class Student\n{\n public string Name { get; set; }\n public string Course { get; set; }\n public int Grade { get; set; }\n}\n\nList<Student> students = new List<Student>\n{\n new Student { Name = "Anna", Course = "Mathematik", Grade = 85 },\n new Student { Name = "Boris", Course = "Mathematik", Grade = 90 },\n new Student { Name = "Anna", Course = "Physik", Grade = 78 },\n new Student { Name = "Boris", Course = "Physik", Grade = 82 },\n new Student { Name = "Anna", Course = "Mathematik", Grade = 95 }\n};\n\nDu möchtest die Durchschnittsnote jedes Studenten in jedem Kurs ermitteln. Wenn du GroupBy nur nach Namen verwendest, werden Annas Daten für Mathematik und Physik vermischt. Wenn du nur nach Kurs gruppierst, geht die Zuordnung zum Studenten verloren. Du benötigst eine Gruppierung nach zwei Spalten: Name + Course.
Die Lösung: Anonymer Typ als zusammengesetzter Schlüssel
\n\nIn C# können wir direkt ein temporäres Objekt mit den benötigten Feldern erstellen:
\n\nvar grouped = students\n .GroupBy(s => new { s.Name, s.Course })\n .Select(g => new\n {\n g.Key.Name,\n g.Key.Course,\n AverageGrade = g.Average(st => st.Grade)\n });\n\nforeach (var item in grouped)\n{\n Console.WriteLine($"{item.Name} | {item.Course} | Durchschnittsnote: {item.AverageGrade:F2}");\n}\n\nAusgabe:
\nAnna | Mathematik | Durchschnittsnote: 90.00
\nBoris | Mathematik | Durchschnittsnote: 90.00
\nAnna | Physik | Durchschnittsnote: 78.00
\nBoris | Physik | Durchschnittsnote: 82.00
Beachte: new { s.Name, s.Course } – das ist der anonyme Typ. LINQ vergleicht Objekte automatisch anhand aller ihrer Eigenschaften (Value equality).
Arbeiten mit IGrouping und verschachtelten Sammlungen
\n\nManchmal ist es notwendig, Daten nicht zu aggregieren, sondern alle ursprünglichen Elemente innerhalb der Gruppe zu behalten. Dafür wird das Interface IGrouping<TKey, TElement> verwendet. Jede Gruppe enthält einen Schlüssel und eine Sammlung von Elementen:
var groups = students.GroupBy(s => new { s.Name, s.Course });\n\nforeach (var group in groups)\n{\n Console.WriteLine($"Student: {group.Key.Name}, Kurs: {group.Key.Course}");\n foreach (var student in group)\n {\n Console.WriteLine($" Note: {student.Grade}");\n }\n}\n\nDieser Ansatz ist ideal, wenn du zum Beispiel einen detaillierten Bericht ausgeben möchtest.
\n\nQuery- vs. Fluent-Syntax – was wählen?
\n\nLINQ unterstützt zwei Stile zum Schreiben von Abfragen. Schauen wir uns beide an:
\n\n1. Fluent (Methodensyntax)
\nvar result = students\n .GroupBy(s => new { s.Name, s.Course })\n .Select(g => new { g.Key.Name, g.Key.Course, Count = g.Count() });\n\n2. Query (SQL-ähnliche Ausdruckssyntax)
\nvar result = from student in students\n group student by new { student.Name, stud