Starting with .NET 11, LINQ has a native operator for full outer joins: FullJoin.
It returns every element from both collections, combining the ones that match and keeping the ones that don’t, with the missing side as null.
Full Joins Before .NET 11
LINQ never had a full outer join. .NET 10 added LeftJoin and RightJoin, but to keep the unmatched items from both sides, you still had to combine a left join with the right-only elements by hand.
What changed in .NET 11?
Now it’s a single method call:
Product[] catalog =
[
new("A100", "Keyboard"),
new("B200", "Mouse"),
];
Sale[] sales =
[
new("B200", 4),
new("C300", 2),
];
var rows = catalog.FullJoin(
sales,
product => product.Sku,
sale => sale.Sku,
(product, sale) => new
{
Sku = product?.Sku ?? sale!.Sku,
ProductName = product?.Name ?? "(not in catalog)",
QuantitySold = sale?.Quantity ?? 0,
});
Here, both parameters of the result selector are nullable, since either side can be missing. That’s why sale!.Sku uses the null-forgiving operator: when there is no product, there is always a sale.
FullJoin and EF Core 11
The same idea works with your database. Just like products without sales and sales without products, customers without orders and orders without customers are both kept.
With EF Core 11, FullJoin is translated directly to a FULL JOIN:
var result = await context.Customers
.FullJoin(
context.Orders,
c => c.Id,
o => o.CustomerId,
(c, o) => new { Customer = c, Order = o })
.ToListAsync();
Generated SQL:
SELECT [c].[Id], [c].[Name], [o].[Id], [o].[CustomerId], [o].[OrderDate]
FROM [Customers] AS [c]
FULL JOIN [Orders] AS [o] ON [c].[Id] = [o].[CustomerId]
Conclusion
FullJoin completes the set of join operators in LINQ, along with Join, LeftJoin and RightJoin, and it works both in memory and with EF Core.
If you are already testing the .NET 11 RC, give it a try. The official release is planned for November, during .NET Conf 2026.
Thank you for reading.
See you next time!



