This issue is made possible thanks to ZZZ Projects, who help keep this newsletter free for everyone. A huge shout-out to them for their support of our community.
EF Core too slow? Insert data 14x faster and cut saving time by 94%.
The âsimpleâ join that was never simple
Every .NET dev whoâs touched a relational database knows LEFT JOIN and RIGHT JOIN.
But in LINQ, writing a proper left join has always felt⌠heavier than it should.
Instead of something obvious like LeftJoin, weâve been chaining GroupJoin, SelectMany, and DefaultIfEmpty in exactly the right order, hoping the next developer remembers the pattern and doesnât âoptimizeâ it into an inner join.
With .NET 10 (LTS, supported until November 2028) and EF Core 10, we finally get first-class LeftJoin and RightJoin operators in LINQ.
These operators read like SQL, translate to proper LEFT JOIN / RIGHT JOIN, and remove a ton of boilerplate.
In this article:
⢠Why were LeftJoin and RightJoin added
⢠How they map to EF Core queries and SQL
⢠Real-world examples youâll actually use in production
⢠Subtle details & limitations you should know before migrating
Before EF Core 10: Left joins were always a ceremony
Letâs remind ourselves what EF Core used to require for a left join:
123456
var query =
from a in A
join b in B on a.Id equals b.AId into g
from b in g.DefaultIfEmpty()
select new { a, b };
Or the even uglier method-syntax variant.
This syntax works, but:
⢠Itâs noisy
⢠Itâs easy to break
⢠juniors constantly forget DefaultIfEmpty()
⢠It doesn't resemble SQL at all
What EF Core 10 adds
You now have:
123
LeftJoin()
RightJoin()
Supported by LINQ-to-Entities, meaning EF Core properly translates them to:
⢠LEFT JOIN
⢠RIGHT JOIN
âŚwith clean, readable, intention-revealing syntax.
Real-World Example #1 (LeftJoin)
Orders + Latest Shipment Tracking
âShow all orders and the date of their most recent shipment, even if the order hasnât been shipped yet.â
This is a real scenario from e-commerce, logistics, and supply-chain systems.
Entities:
1234567891011121314
public class Order
{
public Guid Id { get; set; }
public string Customer { get; set; } = default!;
public DateTime Created { get; set; }
}
public class Shipment
{
public Guid Id { get; set; }
public Guid OrderId { get; set; }
public DateTime Sent { get; set; }
}
EF Core 10 LeftJoin Query
We first compute a âlatest shipment per orderâ:
12345678
var latestShipments = db.Shipments
.GroupBy(s => s.OrderId)
.Select(g => new
{
OrderId = g.Key,
LastShipment = g.Max(x => x.Sent)
});
Then we join orders with (optional) shipment info:
123456789101112131415
var query = db.Orders
.LeftJoin(
latestShipments,
order => order.Id,
shipment => shipment.OrderId,
(order, shipment) => new
{
order.Id,
order.Customer,
order.Created,
LastShipment = shipment?.LastShipment
}
)
.OrderByDescending(x => x.Created);
Why this example matters
⢠Itâs a real-world ecommerce/logistics use case.
⢠It shows a typical âlatest entry per groupâ pattern.
⢠It leverages grouping + left join in a clean, readable way
Real-World Example #2 (RightJoin)
Employee Directory + Optional Workstation Assignment
âShow all workstations and who is assigned to them, including empty workstations.â
This is extremely common in:
⢠office management
⢠manufacturing floors
⢠call centers
⢠corporate IT asset management
Entities:
12345678910111213
public class Workstation
{
public int Id { get; set; }
public string Location { get; set; } = default!;
}
public class Employee
{
public Guid Id { get; set; }
public string Name { get; set; } = default!;
public int? StationId { get; set; }
}
EF Core 10 RightJoin Query
1234567891011121314
var query = db.Employees
.RightJoin(
db.Workstations,
emp => emp.StationId,
ws => ws.Id,
(emp, ws) => new
{
WorkstationId = ws.Id,
ws.Location,
EmployeeName = emp?.Name
}
)
.OrderBy(x => x.WorkstationId);
Why this example matters
⢠RightJoin makes sense here because the right table (workstations) is the primary dataset.
⢠It reflects real organizational data flows.
⢠It shows how to handle optional assignments cleanly.
What about LINQ query syntax?
If youâre hoping for a new C# keyword like:
1234
from p in Products
left join r in Reviews on p.Id equals r.ProductId
select ...
âŚthatâs not what shipped in .NET 10.
As of .NET 10 / C# 14, LeftJoin and RightJoin are extension methods only on IQueryable / IEnumerable. There are no new query-expression keywords for left/right joins, and the existing join in query syntax still behaves the same way as before (inner join, plus the old group join + DefaultIfEmpty pattern for left joins).
So you effectively have two options:
1. Stick to method syntax for joins
This is the âhappy pathâ for EF Core 10 and the one the team clearly optimized for:
12345678
var query = db.Orders
.LeftJoin(
db.Shipments,
order => order.Id,
shipment => shipment.OrderId,
(order, shipment) => new { order, shipment }
);
This gives you a clear intention and first-class translation to SQL LEFT JOIN / RIGHT JOIN.
2. Use query syntax around method calls (if you really want)
You can technically wrap a method-based join inside a query expression:
123456789
var query =
from x in db.Orders.LeftJoin(
db.Shipments,
o => o.Id,
s => s.OrderId,
(o, s) => new { o, s })
where x.o.Created >= cutoff
select new { x.o.Id, x.s?.Sent };
But this is really just query syntax âaroundâ the method chain. Youâre not getting new query operators - youâre still using the LeftJoin extension method under the hood.
In practice, most teams that adopt LeftJoin / RightJoin will:
⢠Keep query syntax for simple from / where / select queries.
⢠Use method syntax (with the new join operators) whenever an outer join is involved.
Thatâs also the most readable compromise in code reviews.
Gotchas and best practices
LeftJoin and RightJoin look simple, but there are a few things worth keeping in mind when you start using them in real EF Core 10 codebases.
1. Always treat one side as nullable
By definition:
⢠LeftJoin â the right side is nullable.
⢠RightJoin â the left side is nullable.
Make that explicit in your projections and avoid accidental NullReferenceExceptions:
123456789101112131415
var result = db.Orders
.LeftJoin(
latestShipments,
o => o.Id,
ls => ls.OrderId,
(o, ls) => new
{
o.Id,
LastShipment = ls?.LastShipment,
ShipmentLabel = ls != null
? $"Shipped at {ls.LastShipment:g}"
: "Not shipped yet"
}
);
This pattern makes intent obvious and avoids the temptation to treat the joined entity as non-null.
2. Keep projections small and focused
Just because you can return entire entities on both sides doesnât mean you should.
Prefer projecting exactly what you need:
⢠Smaller SQL result sets
⢠Less data materialization
⢠Faster queries and less memory pressure
1234567
select new OrderSummaryDto
{
Id = o.Id,
Customer = o.Customer,
LastShipment = ls?.LastShipment
};
This is especially important when you join large tables (orders, events, logs, telemetry, etc.).
3. Index your join keys
LeftJoin/RightJoin donât magically optimize the database - they still compile to regular SQL joins. The usual relational rule still applies:
Typical examples:
⢠FK columns: Shipments.OrderId, Reviews.ProductId, JobExecution.JobId
⢠Business keys used in joins: ClientId, ExternalId, BankReference
Without indexes, large outer joins will happily turn into table scans.
Conclusion
.NET 10 and EF Core 10 are a big release - complex types, improved JSON support, named query filters, better ExecuteUpdate, and more.
But for everyday data access, LeftJoin and RightJoin quietly fix one of the most annoying gaps in LINQ:
⢠No more GroupJoin + SelectMany + DefaultIfEmpty rituals
⢠Queries that look like LEFT JOIN / RIGHT JOIN
⢠Cleaner, more maintainable EF Core code that you can safely hand to juniors
If you have codebases full of hand-rolled left joins:
⢠Start by refactoring the ugliest ones into LeftJoin
⢠Wire them into real features - dashboards, reports, export pipelines
⢠Use this as a teaching moment for your team: âThis is how we express joins in EF Core 10 from now on.â
And because this is an LTS release, you can confidently adopt these operators in production and enjoy them for years.
That's all from me for today.