How to join collections with LINQ in C#
The LINQ Join() method combines two sequences on a matching key — equivalent to a SQL inner join. Elements from both sides that share the same key are combined into a result; elements with no match are excluded.
For left-join behavior (keep all elements from the left, even without a match), use GroupJoin(). To combine every combination of two sequences, use SelectMany() as a cross join.
Basic Inner Join
C# Example Code
var customers = new List<Customer>
{
new Customer(1, "Alice"),
new Customer(2, "Bob"),
new Customer(3, "Charlie")
};
var orders = new List<Order>
{
new Order(101, 1, "Laptop"),
new Order(102, 2, "Mouse"),
new Order(103, 1, "Keyboard"),
new Order(104, 2, "Monitor")
};
// Join customers with their orders
var result = customers.Join(
orders,
customer => customer.Id, // outer key
order => order.CustomerId, // inner key
(customer, order) => new // result selector
{
customer.Name,
order.OrderId,
order.Product
}
);
foreach (var item in result)
Console.WriteLine($"{item.Name} — Order #{item.OrderId}: {item.Product}");
// Output:
// Alice — Order #101: Laptop
// Alice — Order #103: Keyboard
// Bob — Order #102: Mouse
// Bob — Order #104: Monitor
// Note: Charlie has no orders and is excluded (inner join)
record Customer(int Id, string Name);
record Order(int OrderId, int CustomerId, string Product);Join — Method Syntax vs Query Syntax
C# Example Code
// Method syntax
var methodJoin = customers.Join(
orders,
c => c.Id,
o => o.CustomerId,
(c, o) => new { c.Name, o.Product }
);
// Query syntax (SQL-like)
var queryJoin = from c in customers
join o in orders on c.Id equals o.CustomerId
select new { c.Name, o.Product };
foreach (var item in queryJoin)
Console.WriteLine($"{item.Name}: {item.Product}");GroupJoin — Simulating a Left Join
GroupJoin() keeps all elements from the left (outer) sequence and groups matching elements from the right (inner) sequence. Combined with SelectMany() and DefaultIfEmpty(), it produces a left join.
C# Example Code
var customers = new List<Customer>
{
new Customer(1, "Alice"),
new Customer(2, "Bob"),
new Customer(3, "Charlie") // no orders
};
var orders = new List<Order>
{
new Order(101, 1, "Laptop"),
new Order(102, 2, "Mouse"),
new Order(103, 1, "Keyboard")
};
// Left join: keep all customers, even those with no orders
var leftJoin = customers
.GroupJoin(
orders,
c => c.Id,
o => o.CustomerId,
(customer, matchingOrders) => new { customer, matchingOrders }
)
.SelectMany(
x => x.matchingOrders.DefaultIfEmpty(), // null if no match
(x, order) => new
{
x.customer.Name,
Product = order?.Product ?? "(no orders)"
}
);
foreach (var item in leftJoin)
Console.WriteLine($"{item.Name}: {item.Product}");
// Output:
// Alice: Laptop
// Alice: Keyboard
// Bob: Mouse
// Charlie: (no orders) ← preserved even with no matchJoin on Multiple Keys
C# Example Code
var inventory = new List<InventoryItem>
{
new InventoryItem("Widget", "Red", 100),
new InventoryItem("Widget", "Blue", 50),
new InventoryItem("Gadget", "Red", 200)
};
var prices = new List<PriceItem>
{
new PriceItem("Widget", "Red", 9.99m),
new PriceItem("Widget", "Blue", 12.99m),
new PriceItem("Gadget", "Red", 24.99m)
};
// Join on composite key (Name + Color)
var priced = inventory.Join(
prices,
i => new { i.Name, i.Color },
p => new { p.Name, p.Color },
(i, p) => new
{
i.Name,
i.Color,
i.Quantity,
p.Price,
TotalValue = i.Quantity * p.Price
}
);
foreach (var item in priced)
Console.WriteLine($"{item.Name} ({item.Color}): {item.Quantity} × {item.Price:C} = {item.TotalValue:C}");
// Output:
// Widget (Red): 100 × $9.99 = $999.00
// Widget (Blue): 50 × $12.99 = $649.50
// Gadget (Red): 200 × $24.99 = $4,998.00
record InventoryItem(string Name, string Color, int Quantity);
record PriceItem(string Name, string Color, decimal Price);Cross Join with SelectMany
A cross join returns every combination of two sequences. There's no dedicated LINQ method — use nested SelectMany() instead.
C# Example Code
var sizes = new List<string> { "Small", "Medium", "Large" };
var colors = new List<string> { "Red", "Blue" };
// Every size × every color
var combinations = sizes.SelectMany(
size => colors,
(size, color) => $"{size} {color}"
);
foreach (var combo in combinations)
Console.WriteLine(combo);
// Output:
// Small Red
// Small Blue
// Medium Red
// Medium Blue
// Large Red
// Large BlueReal-World Example: Orders with Customer Details and Products
C# Example Code
var customers = new List<Customer>
{
new Customer(1, "Alice"),
new Customer(2, "Bob")
};
var orders = new List<Order>
{
new Order(101, 1, "Laptop"),
new Order(102, 1, "Mouse"),
new Order(103, 2, "Keyboard")
};
var report = orders
.Join(
customers,
o => o.CustomerId,
c => c.Id,
(o, c) => new
{
c.Name,
o.OrderId,
o.Product
}
)
.OrderBy(r => r.Name)
.ThenBy(r => r.OrderId);
Console.WriteLine("Customer Order Report:");
foreach (var row in report)
Console.WriteLine($" [{row.OrderId}] {row.Name} → {row.Product}");
// Output:
// Customer Order Report:
// [101] Alice → Laptop
// [102] Alice → Mouse
// [103] Bob → KeyboardJoin Types — Quick Comparison
| Join Type | LINQ Method | SQL Equivalent | Excludes non-matches? |
|---|---|---|---|
| Inner join | Join() | INNER JOIN | Yes (both sides must match) |
| Left join | GroupJoin() + SelectMany() + DefaultIfEmpty() | LEFT JOIN | No (left side always included) |
| Cross join | SelectMany() | CROSS JOIN | N/A (all combinations) |
| Group join | GroupJoin() | — | No (groups inner into a list) |