2012-02-03 14 views
7

Busco una manera de combinar dos expresiones lambda, sin utilizar un Expression.Invoke a cada expresión. En esencia, quiero construir una nueva expresión que encadene dos separadas. Considere el siguiente código:Combinar Expresiones Lambda

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; } 
} 

Y digamos que tenía dos expresiones:

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

Y quiero unirlas para obtener funcionalmente la siguiente expresión:

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

El único que se me ocurrió hacer esto es utilizar un ExpressionVisitor así:

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); 
    } 
} 

Y entonces su utilizarse como esto:

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

Funciona, pero se siente un poco torpe para mí. Todavía estoy tratando de envolver mis expresiones de cabeza y me pregunto si hay una manera más idiomática de expresar esto, y tengo la sospecha astuta de que me falta algo obvio.


La razón Quiero hacer esto es para usarlo con los ASP.net MVC 3 ayudantes HTML. Tengo algunas ViewModels profundamente anidados y algunas extensiones HtmlHelper que ayudan a tratar con aquellos, por lo que la expresión tiene que ser sólo una colección de MemberExpressions para el construido en el MVC ayudantes para procesar correctamente y construir el nombre correctamente anidado valores de atributo. Mi primer instinto fue utilizar Expression.Invoke() e invocar la primera expresión y encadenarla a la segunda, pero a los ayudantes MVC no les gustó mucho. Perdió su contexto jerárquico.

Respuesta

21

Use un visitante a cambiar todas las instancias del parámetro f a m.SubModel.Foo, y crear una nueva expresión con m como el parámetro:

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 Esto tiene mucho sentido ahora que lo veo. Una cosa que no mencioné en la pregunta original: ¿hay alguna manera de hacer esto sin mutar ni la expresión inicial? Por ejemplo, tengo una expresión principal que necesito extender de varias maneras diferentes, generando nuevas expresiones con cada invocación. –

+0

@ 32bitkid ¡sí! la expresión es inmutable; ¡No he mutado a ninguno de ellos! –

+0

Muchas gracias por su ayuda nuevamente. –

6

Su solución parece delimitar estrictamente a su problema específico, lo que parece inflexible.

Me parece que puede resolver su problema de manera sencilla mediante la simple sustitución lambda: reemplace instancias del parámetro (o "variable libre" como lo llaman en cálculo lambda) con el cuerpo. (Véase la respuesta de Marc como algo de código para hacerlo.)

Desde parámetro en los árboles de expresión tiene identidad referencial en lugar de la identidad de valor no es ni siquiera una necesidad de cambiar el nombre de alfa ellos.

Es decir, usted tiene:

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 

y desea producir la composición

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

Así que toma el cuerpo g(b), hacer una búsqueda y reemplazo visitante en busca de la ParameterExpression de b, y reemplazarlo con el cuerpo f(a) para darle la nueva g(f(a)) cuerpo.Luego crea una nueva lambda con el parámetro a que tenga ese cuerpo.

+0

@Kobi: No entiendo la pregunta. ¿* Qué * todavía es posible? Y 'a' y' b' no son lambdas; son parámetros formales. –

+0

¿Es esta solución adecuada para dos 'Expression's, o solo para dos' Func <> 's? (vale, no importa, creo que entiendo tu respuesta ahora) – Kobi

+0

@Kobi: No entiendo lo que quieres decir con "adecuado". Supongamos que tiene la expresión para la constante 1 y la expresión para la constante 2. ¿Qué operación desea realizar en estas dos expresiones que sea análoga a la composición de la función en lambdas? –

0

Actualización: la respuesta siguiente genera una "Invocación" que EF no admite.

Sé que este es un hilo viejo, pero tengo la misma necesidad y me di cuenta de una manera más limpia de hacerlo. Suponiendo que puede alterar la "expresión 2" a un usuario lambda genérica, se puede inyectar una como esta:

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" 
    } 
}