Combina le espressioni Lambda

asp.net-mvc-3 c# expression-trees

Domanda

Sto cercando un modo per combinare due espressioni lambda, senza usare Expression.Invoke su entrambe le espressioni. Voglio essenzialmente costruire una nuova espressione che ne incatena due separati. Considera il seguente codice:

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 diciamo che avevo due espressioni:

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

E voglio unirli insieme per ottenere funzionalmente la seguente espressione:

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

L'unico modo in cui potrei pensare di farlo è di usare 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 è usato in questo modo:

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

Funziona, ma mi sembra un po 'goffo. 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 per cui voglio farlo è usarlo con gli helper HTML mvc 3 di ASP.net. Ho alcuni ViewModels profondamente nidificati e alcune estensioni HtmlHelper che aiutano a MemberExpressions , 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 accettata

Utilizzare un visitatore per scambiare tutte le istanze del parametro f su 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);
    }
}

Risposta popolare

La tua soluzione sembra essere strettamente adattata al tuo problema specifico, che sembra inflessibile.

Mi sembra che tu possa risolvere il tuo problema abbastanza facilmente con una semplice sostituzione lambda: sostituisci le istanze del parametro (o "variabile libera" come la chiamano nel calcolo lambda) con il corpo. (Vedi 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è, hai:

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 desideri produrre la composizione

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

Quindi prendi il corpo g(b) , fai una ricerca e sostituisci il visitatore alla ricerca di ParameterExpression per b , e sostituiscilo 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.



Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché