Qualcuno può spiegarmi perché il motore EF non sta funzionando nel seguente scenario?
Funziona bene con la seguente espressione:
var data = context.Programs
.Select(d => new MyDataDto
{
ProgramId = d.ProgramId,
ProgramName = d.ProgramName,
ClientId = d.ClientId,
Protocols = d.Protocols.Where(p => p.UserProtocols.Any(u => u.UserId == userId))
.Count(pr => pr.Programs.Any(pg => pg.ProgramId == d.ProgramId))
})
.ToList();
Ma se incapsulare alcuni in un metodo di estensione:
public static IQueryable<Protocol> ForUser(this IQueryable<Protocol> protocols, int userId)
{
return protocols.Where(p => p.UserProtocols.Any(u => u.UserId == userId));
}
La query risultante:
var data = context.Programs
.Select(d => new MyDataDto
{
ProgramId = d.ProgramId,
ProgramName = d.ProgramName,
ClientId = d.ClientId,
Protocols = d.Protocols.ForUser(userId)
.Count(pr => pr.Programs.Any(pg => pg.ProgramId == d.ProgramId))
})
.ToList();
Fallisce con l'eccezione: LINQ to Entities non riconosce il metodo 'System.Linq.IQueryable1 [DAL.Protocol] ForUser (System.Linq.IQueryable1 [DAL.Protocol], Int32)' metodo, e questo metodo non può essere tradotto in un Espressione del negozio.
Mi aspetto che l'EF Engine costruisca l'intero albero delle espressioni, concatenando le espressioni necessarie e quindi generando l'SQL. Perché non lo fa?
Ciò sta accadendo perché la chiamata a ForUser()
viene effettuata all'interno dell'albero delle espressioni che il compilatore C # costruisce quando vede il lambda che si passa in Select. Entity Framework cerca di capire come convertire quella funzione in SQL, ma non può invocare la funzione per alcuni motivi (ad esempio d.Protocols
non esiste al momento).
L'approccio più semplice che funziona per un caso come questo è di avere l'helper che restituisce un'espressione lambda di criteri, e poi lo passa nel metodo .Where()
:
public static Expression<Func<Protocol, true>> ProtocolIsForUser(int userId)
{
return p => p.UserProtocols.Any(u => u.UserId == userId);
}
...
var protocolCriteria = Helpers.ProtocolIsForUser(userId);
var data = context.Programs
.Select(d => new MyDataDto
{
ProgramId = d.ProgramId,
ProgramName = d.ProgramName,
ClientId = d.ClientId,
Protocols = d.Protocols.Count(protocolCriteria)
})
.ToList();
Quando invochi un metodo LINQ all'esterno di un albero di espressioni (come fai con context.Programs.Select(...)
), il metodo di estensione Queryable.Select()
viene effettivamente richiamato e la sua implementazione restituisce un IQueryable<>
che rappresenta il metodo di estensione chiamato su IQueryable<>
originale IQueryable<>
. Ecco l'implementazione di Select, ad esempio:
public static IQueryable<TResult> Select<TSource,TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector) {
if (source == null)
throw Error.ArgumentNull("source");
if (selector == null)
throw Error.ArgumentNull("selector");
return source.Provider.CreateQuery<TResult>(
Expression.Call(
null,
GetMethodInfo(Queryable.Select, source, selector),
new Expression[] { source.Expression, Expression.Quote(selector) }
));
}
Quando il provider di queryable deve generare dati effettivi da IQueryable<>
, analizza l'albero di espressioni e tenta di capire come interpretare tali chiamate di metodo. Entity Framework ha una conoscenza incorporata di molte funzioni correlate a LINQ come .Where()
e .Select()
, quindi sa come tradurre quelle chiamate di metodo in SQL. Tuttavia, non sa cosa fare per i metodi che scrivi.
Quindi, perché funziona?
var data = context.Programs.ForUser(userId);
La risposta è che il metodo ForUser
non è implementato come il metodo Select
sopra: non si aggiunge un'espressione ForUser
interrogabile per rappresentare la chiamata a ForUser
. Invece, si restituisce il risultato di una chiamata .Where()
. Dal punto di vista di IQueryable<>
, è come se Where()
stato chiamato direttamente e la chiamata a ForUser()
non sia mai avvenuta.
Puoi dimostrarlo catturando la proprietà Expression
su IQueryable<>
:
Console.WriteLine(data.Expression.ToString());
... che produrrà qualcosa del genere:
Programs.Where(u => (u.UserId == value(Helpers<>c__DisplayClass1_0).userId))
Non c'è nessuna chiamata a ForUser()
nessuna parte di quell'espressione.
D'altra parte, se includi la chiamata ForUser()
all'interno di un albero di espressioni come questo:
var data = context.Programs.Select(d => d.Protocols.ForUser(id));
... quindi il metodo .ForUser()
non viene mai richiamato, quindi non restituisce mai un IQueryable<>
che conosce il metodo .Where()
ottenuto. Invece, l'albero delle espressioni per gli .ForUser()
interrogabili mostra .ForUser()
viene richiamato . L'output del suo albero delle espressioni sarebbe simile a questo:
Programs.Select(d => d.Protocols.ForUser(value(Repository<>c__DisplayClass1_0).userId))
Entity Framework non ha idea di cosa dovrebbe fare ForUser()
. Per quanto è interessato, potresti aver scritto ForUser()
per fare qualcosa che è impossibile fare in SQL. Quindi ti dice che non è un metodo supportato.