1. Einführung
Stell dir eine klassische Aufgabe vor: Du hast zwei Collections, die logisch miteinander verbunden sind. Zum Beispiel eine Liste von Produktkategorien und eine Liste der Produkte selbst. Du willst für jede Kategorie alle Produkte dieser Kategorie bekommen. Oder du hast eine Liste von Abteilungen in einer Firma und eine Liste von Mitarbeitern, und willst für jede Abteilung alle Mitarbeiter anzeigen.
In SQL nennt man das "gruppiertes Join" (GROUP JOIN oder genauer gesagt LEFT OUTER JOIN mit Gruppierung). In LINQ gibt es dafür einen speziellen Operator – GroupJoin. Das ist so etwas wie ein Zwischending zwischen dem normalen Join (Join), wo jedem linken Eintrag genau ein rechter entspricht, und einer Gruppierung nach Schlüssel. GroupJoin verbindet jedes Element einer Collection mit allen passenden Elementen aus der zweiten Collection als Collection.
Analogie
Wenn ein normaler Join so ist, als würde man Vater-und-Sohn-Paare nach Nachnamen verbinden, dann ist GroupJoin wie einen Baum bauen: Zu jedem Vater kommt eine Liste aller seiner Kinder.
Schematisch
Kategorien Produkte
+--------------+ +---------------------+
| Id | Name | | Name | CatId |
+----+---------+ +-----------+---------+
| 1 | Brot | ---> | Baguette | 1 |
| 2 | Getränke| | Wurst | 3 |
| 3 | Fleisch | | Pepsi | 2 |
| | | | Tee | 2 |
+----+---------+ +-----------+---------+
Nach GroupJoin:
- Brot — [Baguette]
- Getränke — [Pepsi, Tee]
- Fleisch — [Wurst]
2. Methodensignatur und Grundbegriffe
Extension-Methode
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer, // "Äußere" Collection (z.B. Kategorien)
IEnumerable<TInner> inner, // "Innere" Collection (z.B. Produkte)
Func<TOuter, TKey> outerKeySelector, // Wie bekommt man den Schlüssel aus dem äußeren Element
Func<TInner, TKey> innerKeySelector, // Wie bekommt man den Schlüssel aus dem inneren Element
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector // Factory für das Ergebnisobjekt/Record
)
- outer: Die Collection, über die iteriert wird und zu der Elemente gejoint werden (z.B. Kategorien).
- inner: Die Collection, aus der die gejointen Elemente kommen (z.B. Produkte).
- outerKeySelector: Lambda, das den Schlüssel für das "linke" Element zurückgibt.
- innerKeySelector: Lambda, das den Schlüssel für das "rechte" Element zurückgibt.
- resultSelector: Funktion, die bestimmt, wie das Ergebnis für jedes Paar (links+Gruppe rechts) aussieht.
3. Praktisches Beispiel: Kategorien und Produkte
Angenommen, wir haben folgende Modelle:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Product
{
public string Name { get; set; }
public int CategoryId { get; set; }
}
Collections für das Beispiel:
var categories = new List<Category>
{
new Category { Id = 1, Name = "Brot" },
new Category { Id = 2, Name = "Getränke" },
new Category { Id = 3, Name = "Fleisch" }
};
var products = new List<Product>
{
new Product { Name = "Baguette", CategoryId = 1 },
new Product { Name = "Pepsi", CategoryId = 2 },
new Product { Name = "Tee", CategoryId = 2 },
new Product { Name = "Wurst", CategoryId = 3 }
};
Verwendung von GroupJoin (Method Syntax)
var groupJoin = categories.GroupJoin(
products,
category => category.Id, // Kategorie-Schlüssel
product => product.CategoryId, // Produkt-Schlüssel
(category, prods) => new // Ergebnis on the fly bauen
{
CategoryName = category.Name,
Products = prods.Select(p => p.Name).ToList() // Liste der Produktnamen dieser Kategorie
}
);
So gehst du das Ergebnis durch:
foreach (var group in groupJoin)
{
Console.WriteLine($"Kategorie: {group.CategoryName}");
foreach (var product in group.Products)
{
Console.WriteLine($" - {product}");
}
}
Ausgabe:
Kategorie: Brot
- Baguette
Kategorie: Getränke
- Pepsi
- Tee
Kategorie: Fleisch
- Wurst
4. GroupJoin: Query Syntax (Abfrage-Syntax)
LINQ unterstützt eine Syntax, die SQL sehr ähnlich ist. Für group join benutzt man das Schlüsselwort join ... into ..., und so eine Abfrage funktioniert fast genauso wie das Beispiel oben.
var groupJoin2 = from c in categories
join p in products on c.Id equals p.CategoryId into prodGroup
select new
{
CategoryName = c.Name,
Products = prodGroup.Select(p => p.Name).ToList()
};
Das ist sehr ähnlich zu einem SQL-Query mit LEFT OUTER JOIN ... GROUP BY.
Visuelles Schema: Wie funktioniert GroupJoin
[Kategorie] [Produkt] Gruppierung (GroupJoin)
Brot --------> Baguette => Brot: [Baguette]
Getränke --------> Pepsi => Getränke: [Pepsi, Tee]
Getränke --------> Tee
Fleisch --------> Wurst => Fleisch: [Wurst]
Jede Kategorie bekommt ihr eigenes “Taschenfach” (IEnumerable<Product>), in das alle Produkte dieser Kategorie landen.
5. Besonderheiten und Stolperfallen
GroupJoin vs. normaler Join
Der Unterschied zwischen normalem Join und GroupJoin liegt in der Anzahl der Ergebnisse. Join gibt für jedes Match ein Paar zurück, GroupJoin gibt für jedes Element der äußeren Collection ein Element zurück, das eine Collection aller passenden Elemente enthält.
Wenn in unserem Schema eine Kategorie ohne Produkte auftaucht, erscheint sie mit GroupJoin trotzdem, nur dass ihre Produkt-Collection leer ist. Dieses Verhalten ist analog zu LEFT OUTER JOIN in SQL (linkes äußeres Join).
Hier ein Beispiel mit einer Kategorie ohne Produkte:
categories.Add(new Category { Id = 4, Name = "Käse" });
var groupJoin3 = categories.GroupJoin(
products,
c => c.Id,
p => p.CategoryId,
(c, prods) => new
{
CategoryName = c.Name,
Products = prods.Select(p => p.Name).ToList()
});
foreach (var group in groupJoin3)
{
Console.WriteLine($"Kategorie: {group.CategoryName}");
if (group.Products.Count == 0)
Console.WriteLine(" (Keine Produkte)");
else
foreach (var product in group.Products)
Console.WriteLine($" - {product}");
}
Kategorie: Brot
- Baguette
Kategorie: Getränke
- Pepsi
- Tee
Kategorie: Fleisch
- Wurst
Kategorie: Käse
(Keine Produkte)
So ein Szenario braucht man oft in Business-Anwendungen: Man will alle Kategorien (oder Gruppen) anzeigen, auch wenn in einer davon keine Elemente sind.
Umsetzung mit GroupBy? Nope!
Viele verwechseln das und versuchen, GroupJoin mit doppeltem GroupBy zu "emulieren". Lass es – GroupJoin ist genau dafür gemacht und macht das "linke Join" nativ.
6. Anwendung mit echten Daten
Zusätzlich zu den vorherigen Vorlesungen lass uns in unsere Lern-App die Möglichkeit einbauen, einen Report auszugeben: “Für jede Kategorie — Liste ihrer Produkte”. Diese Aufgabe braucht man oft in Online-Shops, CRM, Buchhaltungs- oder Reporting-Systemen.
Fügen wir Code in unsere Demo-App ein:
// Angenommen, wir haben die Klassen Category und Product und die Collections sind schon erstellt
Console.WriteLine("REPORT NACH KATEGORIEN UND PRODUKTEN:");
var categoryReport = categories.GroupJoin(
products,
cat => cat.Id,
prod => prod.CategoryId,
(cat, prods) => new
{
cat.Name,
ProductNames = prods.Select(p => p.Name).ToList()
});
foreach (var row in categoryReport)
{
Console.WriteLine($"Kategorie: {row.Name}");
if (row.ProductNames.Count == 0)
Console.WriteLine(" (Keine Waren)");
else
foreach (var prodName in row.ProductNames)
Console.WriteLine($" - {prodName}");
}
7. Verschachtelte Gruppierungen und Arbeit mit Aggregaten
GroupJoin kann man mit Aggregatfunktionen kombinieren, um komplexere Reports zu bauen.
Beispiel: Die Anzahl der Produkte in jeder Kategorie zählen
var reportWithCount = categories.GroupJoin(
products,
category => category.Id,
product => product.CategoryId,
(category, prods) => new
{
Category = category.Name,
Count = prods.Count() // Aggregatfunktion!
});
foreach (var rec in reportWithCount)
{
Console.WriteLine($"{rec.Category}: {rec.Count} Waren");
}
Angenommen, die Collections categories und products enthalten folgende Daten:
var categories = new[]
{
new { Id = 1, Name = "Obst" },
new { Id = 2, Name = "Gemüse" },
new { Id = 3, Name = "Milchprodukte" }
};
var products = new[]
{
new { Id = 1, Name = "Apfel", CategoryId = 1 },
new { Id = 2, Name = "Banane", CategoryId = 1 },
new { Id = 3, Name = "Karotte", CategoryId = 2 }
};
Die Konsolenausgabe sieht so aus:
Kategorie: Obst — 2 Waren
Kategorie: Gemüse — 1 Waren
Kategorie: Milchprodukte — 0 Waren
8. GroupJoin und typische Anfängerfehler
Ein häufiger Fehler ist zu erwarten, dass das Ergebnis von GroupJoin eine flache Tabelle von Paaren ist, wie beim normalen Join. Mit "flach" ist hier eine Struktur gemeint, bei der jede Zeile ein Paar darstellt: ein äußeres Element und ein passendes inneres (wie eine SQL-Tabelle nach INNER JOIN).
Das verwirrt besonders Leute, die schon ein bisschen mit Datenbanken gearbeitet haben: Sie erwarten, dass GroupJoin sich wie LEFT JOIN verhält, aber Paare in Zeilen zurückgibt, nicht Gruppen. Aber GroupJoin gibt ein Element aus der äußeren Collection und eine Collection der passenden inneren Elemente zurück – also eine verschachtelte Struktur.
Vergiss nicht, die verschachtelten Collections bei Bedarf "aufzuklappen" – zum Beispiel mit SelectMany, wenn du eine normale Sequenz von Paaren willst.
Ein weiterer häufiger Fehler: Man vergisst, dass für Elemente ohne Match die Untergruppe einfach eine leere Liste ist. Das ist das Standardverhalten, kein Fehler – aber wichtig, damit du dich nicht wunderst, warum "nichts passiert" in der Ausgabe.
Wann sollte man GroupJoin verwenden und wann nicht?
Verwende GroupJoin:
- Wenn du zwei Datensätze hast (z.B. Abteilungen und Mitarbeiter, Kategorien und Produkte) und sie hierarchisch anzeigen willst: Für jeden “Eltern” alle seine “Kinder”.
- Für komplexe Reports, wo du alle Hauptelemente zeigen willst, auch wenn es keine “Nachkommen” gibt.
- Wenn du ein SQL-LEFT OUTER JOIN mit Gruppierung nach Schlüssel brauchst.
Verwende GroupJoin nicht, wenn du einfach nur Collections schneiden oder genau ein Paar pro Match willst – dafür gibt es den normalen Join.
GO TO FULL VERSION