What's New in EF Core 10: LeftJoin and RightJoin in LINQ
Learn the new LeftJoin and RightJoin LINQ operators in EF Core 10. Simple examples, SQL mapping, and clear tables to help you write cleaner join queries.
Joining two tables is one of the most common things we do with a database. EF Core 10 makes one kind of join, the left join, much cleaner to write. It also adds a matching right join. This post walks through both, with simple words and small examples.
A everyday story first
Imagine your school is taking a class photo. The teacher has a list of every student. The teacher also has a list of clubs, like the chess club and the music club.
Now the teacher wants one big list: every student, and the club they belong to. But here is the catch. Some students have not joined any club yet.
If you only keep students who are in a club, you lose the others. That is not fair. The teacher wants all students on the list. For students with no club, the club column just stays empty.
That "keep everyone on the left, fill empty where there is no match" idea is exactly a left join. The students are the left list. The clubs are the right list. Students with no club still appear, with a blank club.
A right join is the mirror image. Keep every club, even clubs that no student joined yet, and fill the student column with a blank when nobody matches.
The old way was painful
Before EF Core 10, LINQ had no plain LeftJoin method. To get a left join you had to chain three things together: GroupJoin, then SelectMany, then DefaultIfEmpty. Many developers had to look this up every single time. It was easy to get wrong.
Here is what that old pattern looked like.
// The old way (still works, but hard to read)
var query = context.Students
.GroupJoin(
context.Departments,
student => student.DepartmentId,
department => department.Id,
(student, departments) => new { student, departments })
.SelectMany(
x => x.departments.DefaultIfEmpty(),
(x, department) => new
{
x.student.FirstName,
x.student.LastName,
Department = department.Name
});Read that again. It is a lot. The word "left join" does not even appear. A new teammate reading this has to decode it before they understand it. That is the problem EF Core 10 fixes.
The new way is short and clear
EF Core 10 adds a real LeftJoin operator. It reads almost like a plain English sentence, and it builds the same SQL underneath.
// The new way in EF Core 10
var query = context.Students
.LeftJoin(
context.Departments,
student => student.DepartmentId, // key on the left side
department => department.Id, // key on the right side
(student, department) => new // shape the result
{
student.FirstName,
student.LastName,
Department = department != null ? department.Name : "No department"
});Look at the four parts of LeftJoin:
- The right collection to join with (
context.Departments). - The key on the left side (
student.DepartmentId). - The key on the right side (
department.Id). - A result shaper that builds the final object.
Notice the department != null check. Because a left join can have no match, the right side may be null. You handle that in the result shaper, just like you would in real life when a student has no department.
How LeftJoin reads
Steps
Right table
context.Departments
Left key
student.DepartmentId
Right key
department.Id
Result
new { ... }
What SQL does EF Core build?
This is the best part. The new LeftJoin is not magic that runs slowly in memory. EF Core translates it into a proper LEFT JOIN in SQL, sent to your database. It is the same SQL the old pattern produced.
// Your EF Core 10 LINQ
var query = context.Students
.LeftJoin(
context.Departments,
s => s.DepartmentId,
d => d.Id,
(s, d) => new { s.FirstName, Dept = d.Name });That becomes roughly this SQL:
SELECT [s].[FirstName], [d].[Name] AS [Dept]
FROM [Students] AS [s]
LEFT JOIN [Departments] AS [d] ON [s].[DepartmentId] = [d].[Id]Clean LINQ in, clean SQL out. No performance loss. Your database does the join, which is what databases are good at.
RightJoin too
EF Core 10 also adds RightJoin. It keeps every row from the right collection, and only the matching rows from the left. Where there is no match, the left side is null.
// RightJoin keeps every department, even empty ones
var query = context.Students
.RightJoin(
context.Departments,
student => student.DepartmentId,
department => department.Id,
(student, department) => new
{
Student = student != null ? student.FirstName : "No student yet",
Department = department.Name
});This translates to a RIGHT JOIN in SQL. In real apps, right joins are used less often than left joins, because you can usually flip the two collections and write a left join instead. But it is nice to have the choice, and sometimes a right join expresses your intent more directly.
Left join vs right join at a glance
Here is a small table to keep the two straight in your head.
| Feature | LeftJoin | RightJoin |
|---|---|---|
| Keeps all rows from | Left collection | Right collection |
| Fills null when no match on | Right side | Left side |
| SQL produced | LEFT JOIN | RIGHT JOIN |
| Most common in real apps | Yes, very common | Less common |
| Available in EF Core 10 | Yes | Yes |
Old pattern vs new operator
This table shows why the change matters so much for daily work.
| Point | Old pattern | New LeftJoin |
|---|---|---|
| Methods needed | GroupJoin + SelectMany + DefaultIfEmpty | Just LeftJoin |
| Readability | Hard, needs decoding | Easy, reads like SQL |
| The words "left join" appear | No | Yes |
| SQL produced | LEFT JOIN | LEFT JOIN (identical) |
| Performance | Same | Same |
| Risk of mistakes | Higher | Lower |
Choosing a join
Steps
Need all of A?
Use LeftJoin with A on left
Need all of B?
Use RightJoin or flip to LeftJoin
Inner join
Use Join for matches only
Inner join, left join, right join
It helps to see all three side by side. An inner join keeps only rows that match on both sides. A left join keeps everything on the left. A right join keeps everything on the right.
Think back to the class photo. An inner join is "only students who are in a club". A left join is "all students, club shown if any". A right join is "all clubs, student shown if any". Same data, different rules about who stays.
What you need to use it
A few simple rules to remember.
- You need .NET 10 and EF Core 10. These operators do not exist on older .NET versions or on .NET Framework.
- The SQL Server, SQLite, and PostgreSQL (Npgsql) providers all support them.
- For now,
LeftJoinandRightJoinare method syntax only. There is no new query keyword. So you cannot write them inside afrom ... select ...query expression yet. You call them as methods. - Always handle the possible
nullon the optional side in your result shaper.
A fuller example
Let's put it together with a tiny model. Say we track blog posts and their authors. Some posts were imported and have no author linked. We still want to list every post.
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public int? AuthorId { get; set; } // nullable: some posts have no author
}
public class Author
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
// Query: every post, with author name or a fallback
var postsWithAuthors = await context.Posts
.LeftJoin(
context.Authors,
post => post.AuthorId,
author => author.Id,
(post, author) => new
{
post.Title,
AuthorName = author != null ? author.Name : "Unknown author"
})
.ToListAsync();Every post shows up. Posts without an author show "Unknown author". The query is short, and a teammate understands it at first glance. That readability is the real win here.
A small tip on null handling
The optional side of a left join can be null. If you forget to check, you can get a NullReferenceException when your code touches a property like author.Name. So keep the habit: in the result shaper, use a null check or the null-conditional operator. For a route like GET /posts/{id} that returns this shaped data, returning a clean fallback string is friendlier to the caller than crashing.
A second real example: orders and customers
Joins are everywhere in business apps. Let's say a shop has customers and orders. The boss wants a report of every customer, and how many orders each one placed. Some customers signed up but never bought anything. The boss still wants to see them, with a count of zero.
This is a left join again. Customers are the left side. We keep them all. Orders are the right side. Customers with no orders still show up.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public decimal Total { get; set; }
}
// Every customer, with the order if any
var report = await context.Customers
.LeftJoin(
context.Orders,
customer => customer.Id,
order => order.CustomerId,
(customer, order) => new
{
customer.Name,
OrderId = order != null ? order.Id : (int?)null,
Spent = order != null ? order.Total : 0m
})
.ToListAsync();A customer named "Asha" who never ordered still appears in the report, with OrderId as null and Spent as 0. A customer who placed three orders shows up three times, once per order. That is normal join behavior. If you want one row per customer with a total, you would group the result afterwards.
Common mistakes to avoid
A few traps catch people when they first use these operators. Keep this list handy.
- Forgetting the null check. The right side of a left join can be
null. Touching a property on it without a check throws aNullReferenceException. Always guard it in the result shaper. - Trying to use query syntax. You cannot write
LeftJoininside afrom x in ... select xblock yet. Use method syntax:context.A.LeftJoin(context.B, ...). - Running on an old .NET version. If your project still targets .NET 8 or 9, the method is not there. Check that your
TargetFrameworkisnet10.0. - Mixing up the key order. The third argument is the left key, the fourth is the right key. Swapping them gives wrong or empty results.
- Expecting one row per left item. A left join still produces one row per matching right item. If a customer has three orders, you get three rows.
Debugging an empty result
Steps
Check keys
Left key vs right key order
Check framework
net10.0 target?
Check nulls
Guard the right side
Why this matters for teams
Code is read far more often than it is written. The old GroupJoin and DefaultIfEmpty pattern was correct, but it slowed people down. Every reader had to pause and translate it in their head. Junior developers often copied it without fully understanding it.
The new LeftJoin operator removes that friction. The query now says what it means. A person who knows SQL can read the LINQ and instantly see the LEFT JOIN. That shared understanding makes code reviews faster and bugs rarer. Small wins like this add up across a big codebase.
References and further reading
- What's New in EF Core 10 (Microsoft Learn)
- What's New in EF Core 10: LeftJoin and RightJoin Operators (Milan Jovanovic)
- Native LeftJoin and RightJoin in .NET 10 (TheCodeMan)
- How LeftJoin and RightJoin Work in EF Core .NET 10 (Round The Code)
Quick recap
- A left join keeps every row from the left collection, and fills the right side with
nullwhen there is no match. - A right join does the mirror: keep every row from the right collection.
- Before EF Core 10 you had to chain
GroupJoin,SelectMany, andDefaultIfEmpty. It worked but was hard to read. - EF Core 10 adds plain
LeftJoinandRightJoinoperators that read like SQL. - They translate to the same
LEFT JOINandRIGHT JOINSQL, so there is no performance loss. - You need .NET 10 and EF Core 10; they are method syntax only for now.
- Always handle the possible
nullon the optional side of the join.
Related Posts
Understanding Change Tracking for Better Performance in EF Core
Learn how EF Core change tracking works, the entity states it uses, and simple tricks like AsNoTracking to make your .NET apps faster.
Calling Views, Stored Procedures and Functions in EF Core
A friendly, beginner guide to calling database views, stored procedures, and functions in EF Core with FromSql, SqlQuery, ExecuteSql, and ToView.
The Real Cost of Returning the Identity Value in EF Core
Why EF Core asking the database for the new Id after every insert costs round trips, and how HiLo, sequences, and Guids cut that cost down.
How to Use Global Query Filters in EF Core (Beginner Guide)
Learn EF Core global query filters with simple examples for soft delete and multi-tenancy, plus the new named filters in EF Core 10.
EF Core Bulk Insert: Boost Performance with Entity Framework Extensions
Learn how EF Core bulk insert with Entity Framework Extensions saves data faster, using simple examples, diagrams, and clear performance comparisons.
Fast SQL Bulk Inserts With C# and EF Core: A Beginner Guide
Learn fast SQL bulk inserts in C# and EF Core using AddRange, batching, SqlBulkCopy, and bulk libraries, with simple diagrams and clear examples.