1. Einführung
Manchmal müssen wir mit zwei verschiedenen Sammlungen arbeiten, die durch ein gemeinsames Merkmal miteinander verbunden sind. Zum Beispiel haben wir eine Liste von Bestellungen und eine separate Sammlung von Kunden. Wie finden wir heraus, welche Bestellung zu welchem Kunden gehört? Dafür müssen beide Sammlungen durch eine gemeinsame ID verbunden sein – zum Beispiel über userId.
In relationalen Datenbanken löst das die Operation JOIN, die Zeilen aus verschiedenen Tabellen anhand übereinstimmender Schlüssel verbindet. In LINQ gibt es einen ähnlichen Operator – der heißt auch Join.
Stell dir zwei Tabellen vor: In der ersten – eine Liste von Bibliothekslesern mit ihren IDs, in der zweiten – eine Liste von Buchbestellungen, bei denen jede Bestellung eine id des Lesers hat. Mit join "kleben" wir diese Tabellen anhand der id zusammen und bekommen am Ende Paare „Leser + seine Bestellung“.
Übrigens, falls du dachtest, join sei nur was für "Datenbanken", hast du echt was verpasst! In der Programmierung muss man Sammlungen oft verbinden, besonders wenn man mit externen Systemen oder strukturierten Dateien (JSON, XML und diversen Excel-Tabellen aus der Buchhaltung) arbeitet.
2. Signatur der Methode Join und Funktionsweise
So sieht die Methode Join aus:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer, // erste Sammlung (äußere)
IEnumerable<TInner> inner, // zweite Sammlung (innere)
Func<TOuter, TKey> outerKeySelector, // wie man den Schlüssel aus dem äußeren Element bekommt
Func<TInner, TKey> innerKeySelector, // wie man den Schlüssel aus dem inneren Element bekommt
Func<TOuter, TInner, TResult> resultSelector) // Funktion zur Ergebnisgenerierung (neues Element)
Auf den ersten Blick wirkt das etwas sperrig, aber wir räumen das gleich auf.
Wie das funktioniert:
Für jedes Element aus der ersten Sammlung (outer) sucht LINQ passende (nach Schlüssel) Elemente aus der zweiten (inner). Wenn die Schlüssel übereinstimmen, wird resultSelector aufgerufen und das Ergebnis in die finale Sammlung gepackt.
3. Beispiel: Kunden und ihre Bestellungen verbinden
Wir legen ein paar Klassen und Sammlungen an. Die Idee ist eine logische Fortsetzung unserer Lern-App (nehmen wir mal an, das ist ein kleiner Shop, den wir im Kurs entwickeln).
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string Product { get; set; } = "";
}
// Sammlung der Kunden
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "Finn" },
new Customer { Id = 2, Name = "Peter" },
new Customer { Id = 3, Name = "Maria" },
};
// Sammlung der Bestellungen
var orders = new List<Order>
{
new Order { Id = 101, CustomerId = 2, Product = "Buch" },
new Order { Id = 102, CustomerId = 1, Product = "Stift" },
new Order { Id = 103, CustomerId = 2, Product = "Heft" },
new Order { Id = 104, CustomerId = 3, Product = "Radiergummi" },
};
Jetzt wollen wir alle Kunden mit ihren Bestellungen bekommen. Zum Beispiel ausgeben: "Peter hat Buch bestellt", "Peter hat Heft bestellt" usw.
Join anwenden
// Kunden mit Bestellungen anhand customer.Id und order.CustomerId verbinden
var query = customers.Join(
orders,
customer => customer.Id, // Wie man den Schlüssel aus dem Kunden bekommt
order => order.CustomerId, // Wie man den Schlüssel aus der Bestellung bekommt
(customer, order) => new // Was tun mit passenden Paaren (neues Objekt erstellen)
{
CustomerName = customer.Name,
Product = order.Product
}
);
// Ergebnis ausgeben
foreach (var item in query)
{
Console.WriteLine($"{item.CustomerName} hat {item.Product} bestellt");
}
Was ausgegeben wird:
Finn hat Stift bestellt
Peter hat Buch bestellt
Peter hat Heft bestellt
Maria hat Radiergummi bestellt
Achte darauf: Wenn ein Kunde mehrere Bestellungen hat, taucht er mehrfach in der Liste auf – jeweils einmal pro Bestellung. Genau so soll es sein!
4. Tabelle: Vergleich Join und GroupBy + SelectMany
| Operation | Ergebnis | Verwendungsszenario |
|---|---|---|
|
Flache Liste von Paaren | Klassisches SQL JOIN. Jedes Paar (Treffer) – eine eigene Zeile. |
|
Gruppen mit Untersammlungen | Du willst "Kunde + alle seine Bestellungen" als Struktur "einer – zu vielen". |
Anfänger nutzen Join oft dort, wo eigentlich eine Struktur „Kunde → Liste der Bestellungen“ gebraucht wird. Aber Join arbeitet zeilenweise und gruppiert nicht. Dafür gibt es GroupJoin – dazu mehr in der nächsten Vorlesung. Oder du nutzt GroupBy.
5. Join in Query Syntax (SQL-Stil LINQ)
LINQ unterstützt zwei Syntax-Varianten: Method-Chaining (Method Syntax, z.B. Join(...)) und den sogenannten SQL-Stil (Query Syntax), der optisch an klassische SQL-Queries erinnert.
Für manche Entwickler – besonders die, die schon mit Datenbanken gearbeitet haben – ist diese Syntax oft anschaulicher:
var query2 =
from customer in customers
join order in orders
on customer.Id equals order.CustomerId
select new
{
CustomerName = customer.Name,
Product = order.Product
};
foreach (var item in query2)
{
Console.WriteLine($"{item.CustomerName} hat {item.Product} bestellt");
}
Achte darauf:
In der Query Syntax wird das Schlüsselwort equals verwendet – der Operator == funktioniert hier nicht!
Das ist eine typische Falle im Vorstellungsgespräch, besonders für Einsteiger 😉
6. Wichtige Details und Stolperfallen
Manchmal kommt es vor, dass nicht alle Elemente aus den Sammlungen im Endergebnis landen. Das liegt daran, dass die Methode Join einen sogenannten „inner join“ macht, also nur die Elemente verbindet, deren Schlüssel übereinstimmen. Wenn ein Kunde keine Bestellungen hat, taucht er einfach nicht in der Ergebnisliste auf. Genauso, wenn es eine Bestellung mit CustomerId gibt, die nicht unter den Kunden ist, landet diese Bestellung auch nicht im Ergebnis.
Aber was, wenn du alle Kunden bekommen willst, auch die ohne Bestellungen? Dafür gibt es den „left join“, der in LINQ mit einer Kombination aus GroupJoin und SelectMany umgesetzt wird. Dazu mehr in der nächsten Vorlesung. Beim klassischen Join müssen beide Seiten vorhanden sein – sonst gibt es keinen Treffer und das Element fliegt raus.
7. Arbeiten mit mehreren Schlüsseln (composite key)
Manchmal muss man Sammlungen nicht nur nach einem, sondern nach mehreren Feldern verbinden. Zum Beispiel: Produkte und deren Verkäufe anhand "Produktcode + Verkaufsjahr" zusammenführen.
In LINQ löst man das, indem man anonyme Objekte als Schlüssel verwendet:
var sales = ...; // Verkäufe
var products = ...; // Produkte
var query = products.Join(
sales,
prod => new { prod.Code, prod.Year },
sale => new { sale.ProductCode, sale.Year },
(prod, sale) => new { prod.Name, sale.Amount }
);
Wichtig ist, dass Typen und Eigenschaftsnamen in diesen anonymen Objekten übereinstimmen. Wenn sie unterschiedlich sind, gibt es keine Treffer – auch wenn die Werte eigentlich gleich sind.
8. Wie sieht die verbundene Sammlung aus – Schema
Hier ein Schema, das die Funktionsweise von join (ganz vereinfacht) zeigt:
flowchart LR
subgraph Customers
A1["Finn (Id=1)"]
A2["Peter (Id=2)"]
A3["Maria (Id=3)"]
end
subgraph Orders
B1["Stift (CustomerId=1)"]
B2["Buch (CustomerId=2)"]
B3["Heft (CustomerId=2)"]
B4["Radiergummi (CustomerId=3)"]
end
A1-->|Id=1|B1
A2-->|Id=2|B2
A2-->|Id=2|B3
A3-->|Id=3|B4
Im Schema sieht man, dass jeder Kunde mit all seinen Bestellungen verbunden wird – sofern die Schlüssel übereinstimmen.
9. Kurz zu Fehlern und Besonderheiten
Einer der häufigsten Fehler ist, die Reihenfolge der Parameter zu verwechseln, besonders wenn die Schlüsseldatentypen in beiden Sammlungen gleich sind. Der Compiler oder LINQ wirft dann keinen Fehler, aber das Ergebnis kann leer oder unerwartet sein.
Eine weitere typische Falle: Mit Join ein „left join“ machen zu wollen, damit alle Kunden im Ergebnis bleiben, auch wenn sie keine Bestellungen haben. Aber der klassische Join arbeitet nur mit passenden Paaren und nimmt keine Elemente ohne Entsprechung auf.
GO TO FULL VERSION