2012-02-03 13 views
7

Sto cercando un modo per combinare due espressioni lambda, senza utilizzare un Expression.Invoke su nessuna espressione. Voglio essenzialmente costruire una nuova espressione che ne incatena due separati. Si consideri il seguente codice:Combine Lambda Expressions

class Model { 
    public SubModel SubModel { get; set;} 
} 

class SubModel { 
    public Foo Foo { get; set; } 
} 

class Foo { 
    public Bar Bar { get; set; } 
} 

class Bar { 
    public string Value { get; set; } 
} 

e permette di dire che ho avuto due espressioni:

Expression<Func<Model, Foo>> expression1 = m => m.SubModel.Foo; 
Expression<Func<Foo, string>> expression2 = f => f.Bar.Value; 

E voglio unirmi a loro insieme per ottenere funzionalmente la seguente espressione:

Expression<Func<Model, string>> joinedExpression = m => m.SubModel.Foo.Bar.Value; 

L'unica modo che potrei pensare di fare questo è quello di utilizzare un ExpressionVisitor come questo:

public class ExpressionExtender<TModel, TIntermediate> : ExpressionVisitor 
{ 
    private readonly Expression<Func<TModel, TIntermediate>> _baseExpression; 

    public ExpressionExtender(Expression<Func<TModel, TIntermediate>> baseExpression) 
    { 
     _baseExpression = baseExpression; 
    } 

    protected override Expression VisitMember(MemberExpression node) 
    { 
     _memberNodes.Push(node.Member.Name); 
     return base.VisitMember(node); 
    } 

    private Stack<string> _memberNodes; 

    public Expression<Func<TModel, T>> Extend<T>(Expression<Func<TIntermediate, T>> extend) 
    { 
     _memberNodes = new Stack<string>(); 
     base.Visit(extend); 
     var propertyExpression = _memberNodes.Aggregate(_baseExpression.Body, Expression.Property); 
     return Expression.Lambda<Func<TModel, T>>(propertyExpression, _baseExpression.Parameters); 
    } 
} 

E poi la sua utilizzati in questo modo:

var expExt = new ExpressionExtender<Model, Foo>(expression1); 
var joinedExpression = expExt.Extend(expression2); 

Funziona, ma ci si sente un po 'goffo per me. Sto ancora cercando di avvolgere le mie espressioni di testa e mi chiedo se c'è un modo più idiomatico per esprimere questo, e ho il minimo sospetto che mi manca qualcosa di ovvio.


Il motivo io voglio fare questo è quello di utilizzare con le aiutanti ASP.net MVC 3 Html. Ho alcuni ViewModels profondamente nidificati e alcune estensioni HtmlHelper che aiutano a gestirli, quindi l'espressione deve essere solo una raccolta di MemberExpressions per gli helper MVC incorporati per elaborarli correttamente e creare i valori degli attributi del nome correttamente nidificati. Il mio primo istinto era usare Expression.Invoke() e invocare la prima espressione e incatenarla al secondo, ma gli assistenti MVC non gli piacevano molto. Ha perso il suo contesto gerarchico.

risposta

21

Utilizzare un visitatore di scambiare tutte le istanze del parametro f a m.SubModel.Foo, e creare una nuova espressione con m come parametro:

internal static class Program 
{ 
    static void Main() 
    { 

     Expression<Func<Model, Foo>> expression1 = m => m.SubModel.Foo; 
     Expression<Func<Foo, string>> expression2 = f => f.Bar.Value; 

     var swap = new SwapVisitor(expression2.Parameters[0], expression1.Body); 
     var lambda = Expression.Lambda<Func<Model, string>>(
       swap.Visit(expression2.Body), expression1.Parameters); 

     // test it worked 
     var func = lambda.Compile(); 
     Model test = new Model {SubModel = new SubModel {Foo = new Foo { 
      Bar = new Bar { Value = "abc"}}}}; 
     Console.WriteLine(func(test)); // "abc" 
    } 
} 
class SwapVisitor : ExpressionVisitor 
{ 
    private readonly Expression from, to; 
    public SwapVisitor(Expression from, Expression to) 
    { 
     this.from = from; 
     this.to = to; 
    } 
    public override Expression Visit(Expression node) 
    { 
     return node == from ? to : base.Visit(node); 
    } 
} 
+0

+1 Questo ha molto senso ora che lo vedo. Una cosa che ho omesso di menzionare nella domanda iniziale: esiste un modo per farlo senza mutare l'espressione di partenza. Ad esempio, ho un'espressione di base che ho bisogno di estendere in molti modi diversi, generando nuove espressioni con ogni invocazione. –

+0

@ 32bitkid si! l'espressione è immutabile; Non ho mutato nessuno dei due! –

+0

Grazie mille per il tuo aiuto. –

6

La soluzione sembra essere strettamente su misura per il vostro problema specifico, che sembra inflessibile.

Mi sembra che si possa risolvere il problema abbastanza facilmente con la semplice sostituzione lambda: sostituire le istanze del parametro (o "variabile libera" come la chiamano nel calcolo lambda) con il corpo. (Vedere la risposta di Marc per un codice per farlo.)

Poiché i parametri negli alberi di espressione hanno un'identità referenziale piuttosto che un'identità di valore, non è nemmeno necessario rinominarli in alpha.

Cioè, avete:

Expression<Func<A, B>> ab = a => f(a); // could be *any* expression using a 
Expression<Func<B, C>> bc = b => g(b); // could be *any* expression using b 

e si desidera produrre la composizione

Expression<Func<A, C>> ac = a => g(f(a)); // replace all b with f(a). 

Quindi prendere il corpo g(b), fare una ricerca e sostituzione dei visitatori alla ricerca del ParameterExpression per , e sostituirlo con il corpo f(a) per darti il ​​nuovo corpo g(f(a)).Quindi crea un nuovo lambda con il parametro a che ha quel corpo.

+0

@Kobi: non capisco la domanda. * Cosa * è ancora possibile? E 'a' e' b' non sono lambda; sono parametri formali. –

+0

Questa soluzione è adatta a due 'Expression's, o solo a due' Func <> 's? (ok, non importa - penso di capire la tua risposta ora) – Kobi

+0

@Kobi: Non capisco cosa intendi per "adatto". Supponiamo di avere l'espressione per la costante 1 e l'espressione per la costante 2. Quale operazione desideri eseguire su queste due espressioni che è analoga alla composizione di funzioni su lambda? –

0

Aggiornamento: la risposta che segue genera un "Invoke" che EF non supporta.

So che questo è un thread vecchio, ma ho lo stesso bisogno e ho trovato un modo più pulito per farlo. Supponendo che tu possa modificare il tuo "expression2" per l'utente un lambda generico, puoi inserire uno come questo:

class Program 
{ 
    private static Expression<Func<T, string>> GetValueFromFoo<T>(Func<T, Foo> getFoo) 
    { 
     return t => getFoo(t).Bar.Value; 
    } 

    static void Main() 
    { 
     Expression<Func<Model, string>> getValueFromBar = GetValueFromFoo<Model>(m => m.SubModel.Foo); 

     // test it worked 
     var func = getValueFromBar.Compile(); 
     Model test = new Model 
     { 
      SubModel = new SubModel 
      { 
       Foo = new Foo 
       { 
        Bar = new Bar { Value = "abc" } 
       } 
      } 
     }; 
     Console.WriteLine(func(test)); // "abc" 
    } 
}