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

Per molti sviluppatori Novembre non è solamente il momento del foliage ma è anche il mese di uscita della nuova versione di .NET… e la versione 10 è finalmente approdata sul pianeta terra!

In questo articolo mi focalizzerò su una piccola grande novità: finalmente abbiamo a disposizione le funzoni leftJoin() e rightJoin() come sintassi LINQ quando operariamo con entity framework Core 10.

Il progetto di esempio di questo articolo è disponibile sul mio Github

.NET e foliage

Left join e Right join

Se avete aperto questo articolo mi aspetto che sappiate già cosa si intenda per left e right join, ma per tutti quelli abituati al vibe coding (😜) ecco a voi un piccolo refresh mentale.

Per questo esempio ho creato un piccolo database strutturato in questo modo:

Ipotizziamo di voler estrarre tutti i personaggi (tabella characters) che non hanno una residenza (tabella residence) su un pianeta (tabella planets).

Le tabelle sono popolate nel seguente modo:

Come potete vedere Han Solo, Grogu e Mando sono senza residenza (ovvio, sono girovaghi! 😁) ed i pianteti Jakku, Naboo e Coruscant risultano essere senza abitanti censiti. Questo database imperiale è decisamente poco aggiornato 😅

Se dovessimo estrarre, tramite una query, tutte le persone senza una residenza dovremmo selezionare la tabella dei personaggi (SELECT) e cercare tutte le corrispondenza nella tabella delle residenza (LEFT JOIN). La query che ne uscirebbe sarebbe la seguente:

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

La right join invece la utilizziamo se partiamo dalla tabella Residences e vogliamo estrarre i suoi legami con la tabella dei Characters.

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

ℹ️ A differenza di una inner join, la left join e la right join estraggono anche le righe che non c’è corrispondenza.

Prima di EF Core 10

Prima di questa release la scrittura di una semplice left o right join era più complicata e di obbligava a scrivere una query custom oppure utilizzare GroupJoin() con il metodo DefaultIfEmpty().

Personalmente ho sempre utilizzato una SqlQueryRaw per mantenere più semplice e leggibile il codice, perdendo i vantaggi di caching di EF ma vincendo dal punto di vista della semplificazione.

Rimanendo sull’esempio di prima le due possibili soluzioni erano queste:

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 
    "
    );

Oppure

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);

La complessità visiva e di scrittura della seconda forma è abbastanza tangibile. Nel mio sviluppo quotidiano io privilegio una scrittura più semplice ad una più complessa, a discapito dello strumento utilizzato: sacrifico l’utilizzo di LINQ (che in molti casi è utile) a quello della query… quantomeno fino ad oggi!

Le nuove funzioni

LINQ, che tramite una sintassi fluent che ci semplifica il lavoro, ha introdotto le funzioni .leftJoin() e .rightJoin(), andando a semplificare la sintassi dell’approccio numero due visto poco fa. La left join si scrive in questo modo:

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

E per la 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

Per semplificare ancora di più confrontiamo la sintassi LINQ con quella SQL:

Il primo parametro delle due funzioni è la tabella da mettere in join, il secondo ed il terzo sono le chiavi delle due tabelle che devono essere messe in join. Il quarto parametro invece è la dichiarazione di un oggetto anonimo (nel mio caso, ma può anche essere un DTO).

Da qui in avanti non è altro che la classica sintassi LINQ e ci permette anche di unire più costrutti join:

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

NB: in questo esempio avrei potuto utilizzare una join anziché la left join iniziale.

Conclusioni

Questa introduzione va a colmare una mancanza abbastanza grande, utile in tutti i contesti dove utilizziamo l’ORM Entity framework. Nel repository Gitlab condiviso ad inizio articolo trovate una base di partenza per poter iniziare a provare e sperimentare, il passo successivo è quello di iniziare ad utilizzarlo nei vostri progetti.

Alla prossima!

Condividi questo articolo
Shareable URL
Post precedente

ASP.NET Core 10 – Le novità!

Prosimo post

Pochi principi per scrivere ottime API REST

Leggi il prossimo articolo