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

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:
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 nullLa right join invece la utilizziamo se partiamo dalla tabella Residences e vogliamo estrarre i suoi legami con la tabella dei Characters.
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:
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
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:
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();
E per la right join:
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();
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:
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();
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!