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 match

Join 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 Blue

Real-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   → Keyboard

Join Types — Quick Comparison

Join TypeLINQ MethodSQL EquivalentExcludes non-matches?
Inner joinJoin()INNER JOINYes (both sides must match)
Left joinGroupJoin() + SelectMany() + DefaultIfEmpty()LEFT JOINNo (left side always included)
Cross joinSelectMany()CROSS JOINN/A (all combinations)
Group joinGroupJoin()No (groups inner into a list)