EF Core 10: .leftJoin() and .rightJoin() in LINQ

For many developers, November isn’t just about fall foliage—it’s also when the new .NET version drops… and version 10 has finally landed!

In this article, I’ll focus on one small significant addition: we finally have leftJoin() and rightJoin() functions available in LINQ syntax when working with Entity Framework Core 10.

The sample project for this article is available on my Github

.NET and foliage

Left join and Right join

If you’ve opened this article, I assume you already know what left and right joins are, but for all vibe coders out there (😜), here’s a quick mental refresh.

For this example, I’ve created a small database structured like this:

Let’s say we want to extract all characters (characters table) who don’t have a residence (residences table) on a planet (planets table).

The tables are populated as follows:

As you can see, Han Solo, Grogu, and Mando have no residence (obviously, they’re wanderers! 😁), and the planets Jakku, Naboo, and Coruscant appear to have no registered inhabitants. This Imperial database is seriously out of date 😅

If we wanted to extract all characters without a residence using a query, we’d select from the characters table (SELECT) and look for all matches in the residences table (LEFT JOIN). The resulting query would look like this:

SQL
select 
  c.Id as 'CharacterId',
  c.Name as 'CharacterName'
from Characters c
left join Residences r on
    c.Id = r.CharacterId
where r.PlanetId is null

We’d use a right join if we started from the Residences table and wanted to extract its relationships with the Characters table.

SQL
select 
  c.Id as 'CharacterId',
  c.Name as 'CharacterName'
from Residences r
right join Characters c on
    c.Id = r.CharacterId
where r.PlanetId is null

ℹ️ Unlike an inner join, left and right joins also return rows where there’s no match.

Before EF Core 10

Prior to this release, writing a simple left or right join was more complicated and forced you to either write a custom query or use GroupJoin() with the DefaultIfEmpty() method.

Personally, I always used SqlQueryRaw to keep the code simpler and more readable, sacrificing EF’s caching advantages but winning on simplification.

Sticking with our earlier example, the two possible solutions were:

C#
var query = _context.Database.SqlQueryRaw<string>(
    @"
        select c.Name
        from Characters c
        left join Residences r on
            c.Id = r.CharacterId
        where r.PlanetId is null 
    "
    );

Or

C#
var query = _context.Characters.GroupJoin(
        _context.Residences,
        residence => residence.Id,
        character => character.CharacterId,
        (character, residenceList) => new { character, residences = residenceList }
    ).SelectMany(
        charResidence => charResidence.residences.DefaultIfEmpty(),
        (charResidence, residence) => new
        {
            CharacterId = charResidence.character.Id,
            CharacterName = charResidence.character.Name,
            PlanetId = residence != null ? residence.PlanetId : (int?)null
        }
    )
    .Where(c => c.PlanetId == null);

The visual and syntactic complexity of the second approach is pretty obvious. In my day-to-day development, I favor simpler code over more complex alternatives, regardless of the tool: I’d sacrifice using LINQ (which is useful in many cases) in favor of raw queries… at least until now!

New functions

LINQ, which simplifies our work through its fluent syntax, has introduced the .leftJoin() and .rightJoin() functions, streamlining the syntax of the second approach we just saw. Here’s how you write a left join:

C#
var query = await _context.Characters.LeftJoin(
    _context.Residences, //table to join
    character => character.Id, //Key to match first table
    residence => residence.CharacterId, //Key to match join table
    (character, residence) => new
    {
        CharacterId = character.Id,
        CharacterName = character.Name,
        PlanetId = residence != null ? residence.PlanetId : (int?)null
    })
    .Where(c => c.PlanetId == null)
    .ToListAsync();
Left join output

And for the right join:

C#
var query = await _context.Residences.RightJoin(
    _context.Characters, //table to join
    residence => residence.CharacterId, //Key to match first table
    character => character.Id, //Key to match join table
    (residence, character) => new
    {
        residenceCharacterId = character.Id,
        residenceCharacterName = character.Name,
        planetId = residence != null ? residence.PlanetId : (int?)null
    }
    )
    .Where(c => c.planetId == null)
    .ToListAsync();
Right join output

To simplify even further, let’s compare the LINQ syntax with SQL:

The first parameter of both functions is the table to join, the second and third are the keys from both tables that should be joined. The fourth parameter is the declaration of an anonymous object (in my case, but it could also be a DTO).

From here on, it’s just standard LINQ syntax, and it even allows us to chain multiple join constructs:

C#
var query = await _context.Characters.LeftJoin(
    _context.Residences,
    character => character.Id,
    residence => residence.CharacterId,
    (character, residence) => new
    {
        CharacterId = character.Id,
        CharacterName = character.Name,
        PlanetId = residence != null ? residence.PlanetId : (int?)null
    })
    .Where(c => c.PlanetId != null)
    .RightJoin(
        _context.Planets,
        charResidence => charResidence.PlanetId,
        planets => planets.Id,
        (charResidence, planets) => new
        {
            CharacterName = charResidence == null ? null : charResidence.CharacterName,
            PlanetName = planets.Name
        }
    )
    .Where(p => p.CharacterName != null)
    .ToListAsync();
Left and right join output

Note: In this example, I could have used a regular join instead of the initial left join.

Conclusion

This introduction fills a pretty significant gap that’s useful in any context where we use the Entity Framework ORM. In the GitHub repository shared at the beginning of this article, you’ll find a starting point to begin experimenting. The next step is to start using it in your own projects.

Until next time!

Share this article
Shareable URL
Prev Post

ASP.NET Core 10 – What’s new!

Next Post

Best practices to design REST APIs the right way

Read next

C# 14

At .NET conf 2025, the new version of C# was unveiled. In this article, we’ll explore the new features and…

Ocelot API Gateway

In modern architectures, especially those based on microservices, the API Gateway represents a fundamental…
Api gateway header