Sostieni AppuntiFacili con una piccola donazione su PayPal
Dona con PayPalIEnumerable<T> e IQueryable<T> sembrano simili perché entrambe si usano con LINQ, ma descrivono due modelli di esecuzione molto diversi:
IEnumerable<T> descrive una sequenza che il runtime C# sa iterare elemento per elemento.IQueryable<T> descrive una query che un provider esterno può analizzare, trasformare e tradurre.La differenza pratica è: con IEnumerable<T> il lavoro avviene tipicamente in memoria, con IQueryable<T> il lavoro può essere spostato verso la sorgente dati (database, provider remoto, motore custom).
graph TD
A[IEnumerable<T>] -->|itera dati gia disponibili| B[LINQ to Objects]
C[IQueryable<T>] -->|espone Expression Tree| D[IQueryProvider]
D --> E[Traduzione SQL o altro linguaggio]
E --> F[Sorgente esterna]
style C fill:#d4edda
In termini di costo, il confronto tipico è:
quando bisogna leggere e filtrare tutti gli elementi in memoria, mentre una query ben indicizzata può avvicinarsi a:
con pari al numero di righe realmente restituite.
IEnumerable<T> è la base dell’iterazione in .NET. Il punto centrale non è il filtro, ma il fatto che una sequenza sappia produrre un enumeratore.
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
foreachQuando scrivi:
foreach (var valore in collezione)
{
Console.WriteLine(valore);
}
il compilatore lo trasforma concettualmente in qualcosa di molto vicino a:
using IEnumerator<int> enumerator = collezione.GetEnumerator();
while (enumerator.MoveNext())
{
int valore = enumerator.Current;
Console.WriteLine(valore);
}
I tre membri chiave del pattern iterator sono:
GetEnumerator(): crea l’oggetto che sa attraversare la sequenza.MoveNext(): avanza alla posizione successiva e restituisce true se esiste un elemento.Current: espone l’elemento corrente.sequenceDiagram
participant Client
participant Enumerable as IEnumerable<T>
participant Enumerator as IEnumerator<T>
Client->>Enumerable: GetEnumerator()
Enumerable-->>Client: Enumerator
loop finche ci sono elementi
Client->>Enumerator: MoveNext()
Enumerator-->>Client: true / false
Client->>Enumerator: Current
Enumerator-->>Client: elemento corrente
end
yield return e la state machine generata dal compilatoreUna delle caratteristiche più importanti di IEnumerable<T> è che puoi implementarlo senza scrivere manualmente un enumeratore completo.
public static IEnumerable<int> NumeriPari(int max)
{
for (int i = 0; i <= max; i++)
{
if (i % 2 == 0)
yield return i;
}
}
Con yield return il compilatore genera una state machine:
IEnumerable<int> e IEnumerator<int>;MoveNext() che riprende l’esecuzione dal punto dell’ultimo yield return.Schema concettuale:
private sealed class NumeriPariStateMachine : IEnumerable<int>, IEnumerator<int>
{
private int _state;
private int _current;
private int _max;
private int _i;
public bool MoveNext()
{
switch (_state)
{
case 0:
_i = 0;
_state = 1;
goto case 1;
case 1:
while (_i <= _max)
{
if (_i % 2 == 0)
{
_current = _i;
_i++;
return true;
}
_i++;
}
break;
}
return false;
}
public int Current => _current;
}
Questo spiega perché IEnumerable<T> può essere molto efficiente quando vuoi streaming di dati uno alla volta invece di creare subito una lista completa.
IEnumerable<T>Su collezioni in memoria, gli operatori LINQ lavorano come composizione di iteratori:
List<int> numeri = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> query = numeri
.Where(n => n > 5)
.OrderBy(n => n);
foreach (var n in query)
Console.WriteLine(n);
Qui non esiste alcuna traduzione SQL: la lambda è codice .NET compilato e la sequenza viene percorsa in memoria.
Se la sequenza è lazy, spesso la memoria aggiuntiva è circa:
oltre allo stato dell’enumeratore. Se invece materializzi con ToList(), la memoria cresce con il numero di elementi:
Questo è il motivo per cui yield return e catene LINQ lazy possono essere molto più leggere di una materializzazione anticipata.
IQueryable<T> estende IEnumerable<T>, ma aggiunge tre concetti fondamentali:
public interface IQueryable<out T> : IEnumerable<T>
{
Expression Expression { get; }
Type ElementType { get; }
IQueryProvider Provider { get; }
}
Expression: l’albero sintattico della query.ElementType: il tipo degli elementi finali.Provider: il componente che sa interpretare l’albero.IQueryProviderUn provider LINQ riceve un Expression Tree e decide come eseguirlo. In Entity Framework Core il flusso concettuale è:
Expression Tree;flowchart LR
A[Query LINQ] --> B[Expression Tree]
B --> C[ExpressionVisitor del provider]
C --> D[Algebra interna del provider]
D --> E[SQL Generator]
E --> F[Database]
F --> G[DbDataReader]
G --> H[Materializzazione entita o DTO]
IQueryable<Prodotto> query = _context.Prodotti
.Where(p => p.Prezzo > 100)
.OrderBy(p => p.Nome);
List<Prodotto> prodotti = await query.ToListAsync();
Fino a ToListAsync() non stai ancora leggendo righe: stai solo accumulando nodi nell’albero di espressione. Il provider osserva qualcosa di concettualmente simile a:
Prodotti;p => p.Prezzo > 100;p => p.Nome.E poi genera SQL del tipo:
SELECT *
FROM Prodotti
WHERE Prezzo > 100
ORDER BY Nome
Gli expression tree sono composti da nodi (MethodCallExpression, BinaryExpression, MemberExpression, ConstantExpression, ecc.). Un provider li percorre con un visitatore:
public class SqlLikeVisitor : ExpressionVisitor
{
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.Name == "Where")
{
// legge il predicato e prepara il frammento WHERE
}
return base.VisitMethodCall(node);
}
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.NodeType == ExpressionType.GreaterThan)
{
// prepara qualcosa come: colonna > valore
}
return base.VisitBinary(node);
}
}
Non è il tuo codice applicativo a eseguire direttamente la lambda: il provider la interpreta come struttura dati.
Sia IEnumerable<T> sia IQueryable<T> supportano spesso la deferred execution: la query viene definita adesso, ma eseguita solo quando servono davvero i risultati.
var query = Enumerable.Range(1, 10)
.Where(n => n > 5)
.Select(n => n * 10);
Console.WriteLine("Query costruita");
foreach (var n in query)
Console.WriteLine(n);
| Operatore | Tipo di esecuzione | Note principali |
|---|---|---|
Where | Deferred | Filtra; con IQueryable diventa spesso WHERE SQL |
Select | Deferred | Proiezione; spesso SELECT SQL |
SelectMany | Deferred | Flatten di sequenze; spesso traducibile in join/apply |
OrderBy / OrderByDescending | Deferred | Imposta ordinamento; in SQL ORDER BY |
ThenBy / ThenByDescending | Deferred | Estende l’ordinamento |
Skip | Deferred | In SQL di solito OFFSET |
Take | Deferred | In SQL di solito TOP o FETCH NEXT |
Distinct | Deferred | In SQL DISTINCT |
GroupBy | Deferred | Su IEnumerable raggruppa in memoria; su IQueryable la traduzione dipende dalla forma della query |
Join | Deferred | Spesso traducibile in JOIN |
GroupJoin | Deferred | Traduzione più delicata; spesso base di left join |
Union | Deferred | In SQL UNION |
Concat | Deferred | In SQL UNION ALL o composizione equivalente |
Append / Prepend | Deferred | Tipico su IEnumerable; non sempre traducibile su provider SQL |
DefaultIfEmpty | Deferred | Utile per left join |
Reverse | Deferred | In memoria è semplice; su SQL non sempre traducibile |
AsEnumerable | Deferred | Cambia il tipo statico e sposta le operazioni successive in memoria |
AsQueryable | Deferred | Espone una sequenza come IQueryable |
ToList / ToListAsync | Immediate | Materializza tutti gli elementi |
ToArray | Immediate | Materializza in array |
ToDictionary | Immediate | Materializza e indicizza per chiave |
Count / LongCount | Immediate | Restituisce un numero; su SQL spesso COUNT(*) |
Any | Immediate | Restituisce bool; su SQL spesso EXISTS |
All | Immediate | Restituisce bool; su SQL viene riscritto in forma equivalente |
First / FirstOrDefault | Immediate | Restituisce il primo elemento |
Single / SingleOrDefault | Immediate | Verifica cardinalità attesa |
Last / LastOrDefault | Immediate | Su SQL può richiedere ordinamento esplicito |
Contains | Immediate se terminale | In SQL può diventare IN o EXISTS |
Sum / Min / Max / Average | Immediate | Operazioni di aggregazione |
Aggregate | Immediate | Molto comune in memoria, raramente traducibile in SQL |
ForEach su List<T> | Immediate | Non è un operatore LINQ standard |
Ogni volta che chiami un operatore terminale come ToList(), Count(), Any() o First(), stai chiudendo la pipeline. Se la sorgente è IQueryable<T>, quello è il punto in cui il provider deve generare e lanciare una query reale.
Gli expression tree sono il cuore di IQueryable<T>.
Func<T, bool> vs Expression<Func<T, bool>>Func<Prodotto, bool> filtroCompilato = p => p.Prezzo > 100;
Expression<Func<Prodotto, bool>> filtroEspressione = p => p.Prezzo > 100;
Differenza concettuale:
Func<Prodotto, bool> è codice compilato: puoi solo eseguirlo.Expression<Func<Prodotto, bool>> è una descrizione dell’espressione: puoi ispezionarla, riscriverla, combinarla, tradurla.Con IEnumerable<T> il metodo Where riceve tipicamente una Func<T, bool>. Con IQueryable<T>, l’overload usa Expression<Func<T, bool>>.
Per l’espressione p => p.Prezzo > 100, l’albero contiene in genere:
ParameterExpression per p;MemberExpression per p.Prezzo;ConstantExpression per 100;BinaryExpression di tipo GreaterThan;LambdaExpression come radice.graph TD
A[Lambda p => ...] --> B[GreaterThan]
B --> C[Member: p.Prezzo]
B --> D[Constant: 100]
Gli expression tree possono essere generati a runtime.
using System.Linq.Expressions;
ParameterExpression parametro = Expression.Parameter(typeof(Prodotto), "p");
MemberExpression proprietaPrezzo = Expression.Property(parametro, nameof(Prodotto.Prezzo));
ConstantExpression valore = Expression.Constant(100m);
BinaryExpression corpo = Expression.GreaterThan(proprietaPrezzo, valore);
Expression<Func<Prodotto, bool>> filtro =
Expression.Lambda<Func<Prodotto, bool>>(corpo, parametro);
Questo è fondamentale per filtri dinamici, specification pattern, builder di query, API di ricerca avanzata.
EF Core applica vari passaggi interni, concettualmente simili a questi:
Se il provider incontra un nodo non traducibile in una parte che deve restare server-side, lancia un’eccezione oppure richiede di passare esplicitamente in memoria con AsEnumerable().
La differenza più importante è dove applichi il filtro, la proiezione e l’aggregazione.
// Carica tutto, poi filtra in memoria
IEnumerable<Prodotto> prodottiEnum = _context.Prodotti;
var costosiMemoria = prodottiEnum.Where(p => p.Prezzo > 100).ToList();
// Compone SQL e filtra sul database
IQueryable<Prodotto> prodottiQuery = _context.Prodotti;
var costosiDb = await prodottiQuery.Where(p => p.Prezzo > 100).ToListAsync();
sequenceDiagram
participant App
participant DB as Database
Note over App,DB: IEnumerable con materializzazione precoce
App->>DB: SELECT * FROM Prodotti
DB-->>App: tutte le righe
App->>App: filtro Where in memoria
Note over App,DB: IQueryable
App->>DB: SELECT ... WHERE Prezzo > 100
DB-->>App: solo le righe utili
Select contro entità completeSpesso l’ottimizzazione più importante non è solo filtrare, ma caricare meno colonne.
// Carica l'entità completa
var prodotti = await _context.Prodotti
.Where(p => p.Prezzo > 100)
.ToListAsync();
// Carica solo le colonne necessarie
var cards = await _context.Prodotti
.Where(p => p.Prezzo > 100)
.Select(p => new ProdottoCardDto
{
Id = p.Id,
Nome = p.Nome,
Prezzo = p.Prezzo
})
.ToListAsync();
Con Select:
Il problema N+1 nasce quando fai una query per ottenere N entità principali e poi, per ciascuna, scateni un’altra query per caricare dati correlati.
var ordini = await _context.Ordini.ToListAsync();
foreach (var ordine in ordini)
{
Console.WriteLine(ordine.Cliente.Nome);
}
Se Cliente viene caricato in lazy loading, puoi ottenere:
Totale: 1 + N round-trip.
Relazione con IEnumerable<T> e IQueryable<T>:
ToList) e poi navighi proprietà non caricate, il rischio di N+1 aumenta;IQueryable<T> puoi fare Include, join o proiezioni per evitare round-trip ripetuti.var ordini = await _context.Ordini
.Select(o => new
{
o.Id,
ClienteNome = o.Cliente.Nome,
Totale = o.Totale
})
.ToListAsync();
Caso semplice: cercare prodotti con Prezzo > 100.
Se hai già caricato tutti i prodotti in memoria, devi spesso attraversare tutti gli elementi:
Su database con indice sulla colonna filtrata, il motore può usare una struttura dati che riduce la ricerca iniziale a:
con righe risultanti. In pratica il guadagno reale dipende da:
// Buffering completo
List<Prodotto> lista = await _context.Prodotti.ToListAsync();
// Streaming asincrono elemento per elemento
await foreach (var prodotto in _context.Prodotti.AsAsyncEnumerable())
{
Console.WriteLine(prodotto.Nome);
}
In termini semplificati:
mentre lo streaming tende a mantenere memoria addizionale proporzionale a pochi elementi alla volta, anche se il provider può avere buffer interni.
Uno dei vantaggi principali di IQueryable<T> è costruire query senza eseguire step intermedi.
public async Task<List<Prodotto>> CercaAsync(
string? nomeContieneParola,
decimal? prezzoMin,
decimal? prezzoMax)
{
IQueryable<Prodotto> query = _context.Prodotti.AsQueryable();
if (!string.IsNullOrWhiteSpace(nomeContieneParola))
query = query.Where(p => p.Nome.Contains(nomeContieneParola));
if (prezzoMin.HasValue)
query = query.Where(p => p.Prezzo >= prezzoMin.Value);
if (prezzoMax.HasValue)
query = query.Where(p => p.Prezzo <= prezzoMax.Value);
return await query.ToListAsync();
}
Il database vede una sola query finale, non tre query separate.
Lo Specification pattern incapsula regole di filtro in oggetti riusabili invece di duplicare lambda sparse nel codice.
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public IQueryable<T> Apply(IQueryable<T> query)
=> query.Where(ToExpression());
}
public sealed class ProdottiCostosiSpecification : Specification<Prodotto>
{
private readonly decimal _prezzoMinimo;
public ProdottiCostosiSpecification(decimal prezzoMinimo)
{
_prezzoMinimo = prezzoMinimo;
}
public override Expression<Func<Prodotto, bool>> ToExpression()
=> p => p.Prezzo >= _prezzoMinimo;
}
Uso:
Specification<Prodotto> spec = new ProdottiCostosiSpecification(100);
var query = spec.Apply(_context.Prodotti);
var risultati = await query.ToListAsync();
Vantaggi:
Expression<Func<T, bool>>).Se al posto di Expression<Func<T, bool>> usassi una Func<T, bool>, perderesti la possibilità di tradurre la regola in SQL.
IQueryable<T> non significa che qualsiasi codice C# possa diventare SQL. Il provider può tradurre solo ciò che conosce.
Questi casi sono spesso problematici o provider-specific:
CalcolaPunteggio(p));Func<T, bool> già compilate;StringComparison non supportati dal provider;try/catch, variabili mutate;Regex.IsMatch(...) se il provider non ha traduzione dedicata;Aggregate arbitrari;OrderBy, Distinct, GroupBy;Where((x, index) => ...);// In genere non traducibile in SQL
var query = _context.Prodotti
.Where(p => AlgoritmoComplesso(p));
Storicamente alcuni provider spostavano in silenzio parti della query sul client. In EF Core moderno, se una porzione centrale della query non è traducibile, nella maggior parte dei casi viene lanciata un’eccezione per evitare degradazioni nascoste di performance.
Il passaggio corretto, se ti serve logica solo C#, è rendere esplicito il confine:
var risultati = _context.Prodotti
.Where(p => p.Prezzo > 10)
.AsEnumerable()
.Where(p => AlgoritmoComplesso(p))
.ToList();
Da quel punto in poi accetti volutamente che il resto del lavoro avvenga in memoria.
IQueryable<T>;AsEnumerable() oppure dopo materializzazione.Questi metodi segnano il confine tra mondi diversi.
AsEnumerable()AsEnumerable() non esegue la query da solo: cambia il tipo statico della pipeline e fa sì che gli operatori successivi vengano risolti come LINQ to Objects.
var query = _context.Prodotti
.Where(p => p.Prezzo > 100)
.AsEnumerable()
.Where(p => AlgoritmoComplesso(p));
Qui:
Where può diventare SQL;Where è sicuramente C# in memoria.AsQueryable()AsQueryable() è utile per esporre una sorgente come query componibile, spesso in test o in librerie generiche.
List<int> lista = new() { 1, 2, 3 };
IQueryable<int> q = lista.AsQueryable();
Attenzione: questo non crea un vero provider SQL. Hai solo un IQueryable<T> basato su LINQ to Objects.
IAsyncEnumerable<T> e await foreachCon dati remoti o stream lunghi, oggi è molto importante anche IAsyncEnumerable<T>, che rappresenta una sequenza asincrona.
await foreach (var prodotto in _context.Prodotti
.Where(p => p.Prezzo > 100)
.AsAsyncEnumerable())
{
Console.WriteLine($"{prodotto.Nome} - {prodotto.Prezzo}");
}
Concetti chiave:
IEnumerable<T>: iterazione sincrona.IAsyncEnumerable<T>: iterazione asincrona elemento per elemento.await foreach: consuma la sequenza senza bloccare il thread durante le attese I/O.A livello di pattern, IAsyncEnumerable<T> usa l’equivalente asincrono dell’iterator pattern:
GetAsyncEnumerator();MoveNextAsync();Current.public async IAsyncEnumerable<int> LeggiNumeriAsync()
{
for (int i = 1; i <= 3; i++)
{
await Task.Delay(100);
yield return i;
}
}
Il compilatore genera anche qui una state machine, ma stavolta combina:
yield return);await);ValueTask<bool> in MoveNextAsync().| Situazione | Scelta consigliata |
|---|---|
Dati già in memoria (List, Array) | IEnumerable<T> |
| Query su database con filtro, sort, paging | IQueryable<T> |
| Regole riusabili traducibili in SQL | Expression<Func<T, bool>> o Specification |
| Logica non traducibile | AsEnumerable() o materializzazione esplicita |
| Stream asincroni di risultati | IAsyncEnumerable<T> |
| Riduzione payload e performance | Select con DTO/proiezioni |
In sintesi:
IEnumerable<T> significa iterazione e composizione in memoria.IQueryable<T> significa descrizione di query traducibile.IAsyncEnumerable<T> significa streaming asincrono.ToList() e simili trasformano una pipeline lazy in risultati bufferizzati.Select è spesso l’ottimizzazione più sottovalutata.IQueryable<T> estende:
Dove vengono eseguiti i filtri LINQ su IEnumerable?
Dove vengono eseguiti i filtri LINQ su IQueryable con EF Core?
Quale metodo forza l'esecuzione immediata di una query LINQ?
Cosa sono gli Expression Trees?
Se usi IEnumerable con EF Core invece di IQueryable:
AsEnumerable() su una IQueryable serve per:
Qual è il vantaggio di costruire query dinamiche con IQueryable?
La deferred execution significa che:
Quale interfaccia è più adatta per la paginazione su database?
Scenario: Vuoi vedere concretamente la differenza tra IEnumerable e IQueryable.
Consegna:
Prodotto (con 1000+ record di test).IEnumerable e con IQueryable per filtrare prodotti con prezzo > 50.optionsBuilder.LogTo(Console.WriteLine)) per vedere le query SQL generate.Obiettivo: vedere visivamente l’impatto di IEnumerable vs IQueryable.
Scenario: Vuoi costruire un endpoint di ricerca prodotti con filtri opzionali.
Consegna:
CercaProdottiAsync(string? nome, decimal? prezzoMin, decimal? prezzoMax, string? categoria).IQueryable<Prodotto> e applica i filtri solo se il parametro è valorizzato.Skip / Take).Obiettivo: costruire query dinamiche efficienti con IQueryable.
Scenario: Hai una query su DB che vuoi completare con un algoritmo C# non traducibile in SQL.
Consegna:
IQueryable.AsEnumerable() per passare in memoria.Obiettivo: capire quando e come usare AsEnumerable() per separare l’esecuzione DB da quella in memoria.