Passa al contenuto principale

LINQ con Entity Framework

Traduzione in SQL

EF traduce le espressioni LINQ in SQL tramite expression tree. Non tutte le operazioni LINQ hanno un equivalente SQL: le operazioni che EF non sa tradurre vengono valutate lato client — cioè EF carica i dati e applica il filtro in C#. Questo è silenzioso e può causare query che portano in memoria interi dataset.

Le query che si vogliono eseguire integralmente su database devono usare solo operazioni traducibili. In caso di dubbio, controllare il log SQL generato.

// Abilitare il logging delle query SQL (solo in development)
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);

Pattern comuni

Filtro, proiezione, paginazione

var pagina = await _db.Ordini
.AsNoTracking()
.Where(o => o.ClienteId == clienteId && o.Stato == StatoOrdine.Confermato)
.OrderByDescending(o => o.DataCreazione)
.Skip((numeroPagina - 1) * dimensionePagina)
.Take(dimensionePagina)
.Select(o => new OrdineDto(o.Id, o.Numero, o.DataCreazione, o.Totale))
.ToListAsync(ct);

Skip e Take vengono tradotti in OFFSET e LIMIT (PostgreSQL). Richiedono sempre un OrderBy per risultati deterministici.

Ricerca con conteggio totale

Per la paginazione con conteggio totale si eseguono due query separate anziché una sola complessa:

var baseQuery = _db.Ordini
.AsNoTracking()
.Where(o => o.ClienteId == clienteId);

var totale = await baseQuery.CountAsync(ct);
var elementi = await baseQuery
.OrderByDescending(o => o.DataCreazione)
.Skip(offset)
.Take(pageSize)
.Select(o => new OrdineDto(o.Id, o.Numero, o.DataCreazione))
.ToListAsync(ct);

Caricamento relazioni con Include

var ordine = await _db.Ordini
.Include(o => o.Cliente)
.Include(o => o.Righe)
.ThenInclude(r => r.Prodotto)
.FirstOrDefaultAsync(o => o.Id == id, ct);

Include genera una JOIN. Più Include sullo stesso livello generano query separate (split query), che in PostgreSQL è spesso più efficiente di una singola query con molte colonne duplicate:

var ordini = await _db.Ordini
.AsSplitQuery() // query separate per ogni Include
.Include(o => o.Righe)
.Include(o => o.Note)
.ToListAsync(ct);

Proiezione con dati da relazioni

La proiezione può accedere alle navigation property senza Include esplicito — EF genera la JOIN automaticamente nella SELECT:

var risultati = await _db.Ordini
.AsNoTracking()
.Where(o => o.Stato == StatoOrdine.Confermato)
.Select(o => new
{
o.Id,
o.Numero,
ClienteNome = o.Cliente.Nome, // JOIN automatica
NumeroRighe = o.Righe.Count, // COUNT in SQL
Totale = o.Righe.Sum(r => r.Importo) // SUM in SQL
})
.ToListAsync(ct);

Any e Count

// Esiste almeno un ordine in attesa?
bool haOrdiniInAttesa = await _db.Ordini
.AnyAsync(o => o.ClienteId == clienteId && o.Stato == StatoOrdine.InAttesa, ct);

// Quanti ordini confermati nel mese corrente?
int conteggio = await _db.Ordini
.CountAsync(o => o.DataCreazione.Month == DateTime.Today.Month, ct);

Any è sempre preferibile a Count() > 0 per verificare l'esistenza: genera EXISTS invece di COUNT(*).

Limiti della traduzione

Alcune operazioni LINQ non vengono tradotte e causano valutazione lato client (o eccezione in EF 3+):

// ❌ Metodi personalizzati non traducibili
.Where(o => IsOrdineValido(o)) // metodo C# — non traducibile

// ❌ Funzioni .NET senza equivalente SQL
.Where(o => o.Numero.IsNormalized())

// ✅ Alternativa: spostare la logica nel database tramite colonne calcolate o filtrare in memoria dopo la query

GroupBy lato server ha limitazioni: EF traduce solo aggregazioni semplici (Count, Sum, Max, Min, Average). Operazioni complesse sul gruppo richiedono valutazione lato client o SQL grezzo.

SQL grezzo

Quando LINQ non è sufficiente — query complesse, CTE, funzioni window — si usa FromSqlRaw o FromSqlInterpolated:

// FromSqlInterpolated è sicuro da SQL injection (usa parametri)
var ordini = await _db.Ordini
.FromSqlInterpolated($"""
SELECT o.*
FROM ordini o
WHERE o.cliente_id = {clienteId}
AND o.data_creazione > now() - interval '30 days'
""")
.AsNoTracking()
.ToListAsync(ct);

FromSqlRaw con concatenazione di stringhe è vulnerabile a SQL injection. Si usa solo con costanti o con EF.Parameter() per i valori dinamici.

Per operazioni che non restituiscono entità (INSERT/UPDATE/DELETE complessi, stored procedure) si usa ExecuteSqlInterpolatedAsync:

await _db.Database.ExecuteSqlInterpolatedAsync(
$"UPDATE prodotti SET scorte = scorte - {quantita} WHERE id = {prodottoId}", ct);