Joins are one of the most fundamental concepts when working with SQL.
They allow you to combine related data from multiple tables based on shared relationships.
Joins are everywhere, you’ll find them in almost every application that deals with relational data.
For example, fetching blog posts with comments, products with reviews or customers with their orders.
However, until now, performing something as common as a Left Join in LINQ required a combination of GroupJoin, SelectMany and DefaultIfEmpty.
While it worked, the syntax was verbose and far from elegant.
With .NET 10, EF Core introduces native LeftJoin and RightJoin operators, dramatically simplifying what used to be a verbose LINQ pattern.
Getting Started
To follow along, we need a new .NET 10 project with simple entities:
public class Order
{
public Guid Id { get; set; }
public List<OrderItem> Items { get; set; }
}
public class OrderItem
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public Guid OrderId { get; set; }
}
Order and OrderItem model will represent a one-to-many relationship.
Left Join
Left Join returns all records from the left table (Orders) and matches them with related records from the right table (OrderItems).
If an order has no items, it still appears in the result with NULL values for the right-side columns.
Here's an example in SQL:
SELECT
o."Id" AS order_id,
oi."Id" AS item_id,
oi."Name" AS item_name,
oi."Price" AS item_price
FROM
"Orders" o
LEFT JOIN
"OrderItems" oi
ON
o."Id" = oi."OrderId";
Before .NET 10, this is how you had to get the same behavior with LINQ queries:
var query =
from o in dbContext.Orders
join oi in dbContext.OrderItems
on o.Id equals oi.OrderId into orderItemsGroup
from oi in orderItemsGroup.DefaultIfEmpty()
select new
{
order_id = o.Id,
item_id = oi != null ? oi.Id : (Guid?)null,
item_name = oi != null ? oi.Name,
item_price = oi != null ? oi.Price : (decimal?)null
};
Honestly, here's a rare example where I favored linq queries compared to linq methods:
var query =
dbContext.Orders
.GroupJoin(
dbContext.OrderItems,
o => o.Id,
i => i.OrderId,
(o, items) => new { o, items })
.SelectMany(
x => x.items.DefaultIfEmpty(),
(x, item) => new
{
OrderId = x.o.Id,
ItemId = item.Id != null ? item.Id : (Guid?)null,
ItemName = item.Name,
ItemPrice = item.Price != null ? item.Price : (decimal?)null
});
With EF Core 10, you can now express this with a single, intuitive call:
var result = await dbContext.Orders.LeftJoin(
dbContext.OrderItems,
order => order.Id,
item => item.OrderId,
(order, item) => new
{
order.Id,
ItemName = item.Name,
ItemPrice = item.Price != null ? item.Price : (decimal?)null
}).ToListAsync();
No GroupJoin, no DefaultIfEmpty, just one simple LeftJoin method.
To perform left join we need to specify a key from the left table that we want to join on and a key from the right table that we want to join on.
Lastly we used projection to shape what the result should look like.
Because the method is implemented by using deferred execution, we need to use ToList in order to materialize the result.
Right Join
Right Join does the opposite, it returns all records from the right table and matches them with those from the left.
Here's an example in SQL:
SELECT
o."Id" AS order_id,
oi."Id" AS item_id,
oi."Name" AS item_name,
oi."Price" AS item_price
FROM
"OrderItems" oi
RIGHT JOIN
"Orders" o
ON
o."Id" = oi."OrderId";
Now, EF Core 10 lets you do this natively too:
var result = await dbContext.OrderItems.RightJoin(
dbContext.Orders,
item => item.OrderId,
order => order.Id,
(item, order) => new
{
order.Id,
ItemName = item.Name,
ItemPrice = item.Price != null ? item.Price : (decimal?)null
}).ToListAsync();
Conclusion
EF Core 10’s introduction of native LeftJoin and RightJoin operators marks a significant simplification how we write join statements.
What once was verbose LINQ can now be expressed in a single, intuitive method call.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
