2013-10-17 18 views
14
Expression<Func<MyObject, string>> fn1 = x => x.PossibleSubPath.MyStringProperty; 

Expression<Func<string, bool>> fn2 = x => x.Contains("some literal"); 

C'è un modo per creare una nuova espressione lambda, che utilizza fondamentalmente l'uscita del Fn1 e lo utilizza come input per fn2?combinare due Linq Espressioni lambda

Expression<Func<MyObject, bool>> fnCombined = ... 

so che posso creare la funzione in una sola volta, ma il problema è che io sto facendo un po 'di codice generico e quindi davvero bisogno di essere in grado di creare queste due funzioni separatamente, poi combinarle in un tale modo che Linq possa usarli sui miei oggetti di database (Entity Framework).

+1

Ecco una risposta che mostra la manipolazione/combinazione di espressioni. http://stackoverflow.com/a/9683506/8155 –

+0

@DavidB Grazie per il collegamento; Stavo avendo difficoltà a capire come sostituire tutte le istanze di un'espressione con un'altra. – Servy

+0

Grazie mille, ragazzi! –

risposta

22

Quindi logicamente ciò che vogliamo essere in grado di fare è creare una nuova lambda in cui ha un parametro di input per la prima funzione, e un corpo che chiama la prima funzione con quel parametro e quindi passa il risultato come il parametro per la seconda funzione, quindi restituisce quello.

Possiamo replicare che abbastanza facilmente usando Expression oggetti:

public static Expression<Func<T1, T3>> Combine<T1, T2, T3>(
    Expression<Func<T1, T2>> first, 
    Expression<Func<T2, T3>> second) 
{ 
    var param = Expression.Parameter(typeof(T1), "param"); 
    var body = Expression.Invoke(second, Expression.Invoke(first, param)); 
    return Expression.Lambda<Func<T1, T3>>(body, param); 
} 

Purtroppo, EF e la maggior parte di altri fornitori di query in realtà non so cosa fare con quello e non funzionerà correttamente. Ogni volta che colpiscono un'espressione Invoke, in genere generano un'eccezione di qualche tipo. Alcuni possono gestire lo stesso. In teoria tutte le informazioni di cui hanno bisogno sono lì, se sono scritte con la robustezza per arrivarci.

Ciò che possiamo fare è, dal punto di vista concettuale, sostituire ogni istanza del primo parametro lambda nel corpo di quella lambda con il parametro di un nuovo lambda che stiamo creando e quindi sostituire tutte le istanze del secondo parametro lambda nella seconda lambda con il nuovo corpo della prima lambda. Tecnicamente, se queste espressioni hanno effetti collaterali, e questi parametri sono usati più di una volta, non sarebbero gli stessi, ma poiché questi verranno analizzati da un provider di query EF, non dovrebbero mai avere effetti collaterali.

Grazie a David B per aver fornito un collegamento a this related question che fornisce un'implementazione ReplaceVisitor. Possiamo usare quello ReplaceVisitor per passare attraverso l'intero albero di un'espressione e sostituire un'espressione con un'altra. L'implementazione di questo tipo è:

class ReplaceVisitor : ExpressionVisitor 
{ 
    private readonly Expression from, to; 
    public ReplaceVisitor(Expression from, Expression to) 
    { 
     this.from = from; 
     this.to = to; 
    } 
    public override Expression Visit(Expression node) 
    { 
     return node == from ? to : base.Visit(node); 
    } 
} 

Ed ora possiamo scrivere il nostro correttaCombine metodo:

public static Expression<Func<T1, T3>> Combine<T1, T2, T3>(
    Expression<Func<T1, T2>> first, 
    Expression<Func<T2, T3>> second) 
{ 
    var param = Expression.Parameter(typeof(T1), "param"); 

    var newFirst = new ReplaceVisitor(first.Parameters.First(), param) 
     .Visit(first.Body); 
    var newSecond = new ReplaceVisitor(second.Parameters.First(), newFirst) 
     .Visit(second.Body); 

    return Expression.Lambda<Func<T1, T3>>(newSecond, param); 
} 

e un semplice banco di prova, per dimostrare proprio quello che sta succedendo:

Expression<Func<MyObject, string>> fn1 = x => x.PossibleSubPath.MyStringProperty; 
Expression<Func<string, bool>> fn2 = x => x.Contains("some literal"); 

var composite = Combine(fn1, fn2); 

Console.WriteLine(composite); 

Quale verrà stampato:

param => param.PossibleSubPath.MyStringProperty.Contains ("un po 'letterale")

che è esattamente quello che vogliamo; un provider di query saprà come analizzare qualcosa del genere.

+0

Se 'fn2' usa il suo parametro più di una volta,' fn1' sarà ripetuto più di una volta. Speriamo che questo sia o banale o gestito in modo intelligente dal provider di query, ma questo potrebbe finire per fare un lavoro significativo da fare più volte (qualcosa come una variabile locale dovrebbe funzionare, eccetto che i provider di query probabilmente non lo supportano). –

+0

@TimS. Sì, ne ho parlato un po 'nella mia risposta. Ad essere onesti, non mi aspetto che sia un problema importante, poiché mi aspetto che il Query Optimizer sul lato DB sia in grado di gestire efficacemente tali casi. Sembrerebbe inoltre che il tipo di espressioni che l'OP sta esaminando non sarebbe un problema. È difficile da dire. Quasi certamente una variabile locale non sarebbe stata in grado di essere analizzata correttamente, come hai detto. – Servy

+0

@Servy, Puoi aggiornare la tua risposta in modo che rispetti, Passare il parametro dal primo corpo, non funzionerà perché quel parametro è definito nell'ambito di un'espressione separata, quindi quando si combina l'espressione 'x => x.condition = = true && x.anotherCondition == false' la seconda x non è la stessa x nella prima. (Probabilmente puoi dire quello che sto cercando di dire meglio) –