结合Lambda表达式

时间:2012-02-03 17:04:19

标签: c# asp.net-mvc-3 expression-trees

我正在寻找一种方法来组合两个lambda表达式,而不在任一表达式上使用Expression.Invoke。我想基本上构建一个链接两个独立的表达式的新表达式。请考虑以下代码:

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

让我说我有两个表达方式:

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

我想和他们一起在功能上获得以下表达式:

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

我能想到的唯一方法是使用这样的ExpressionVisitor:

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

然后就像这样使用:

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

它有效,但对我来说感觉有点笨拙。我仍然试图包裹我的头脑表达,并想知道是否有一种更惯用的方式来表达这一点,我有一种偷偷摸摸的怀疑,我错过了一些明显的东西。


原因我想这样做是为了将它与ASP.net mvc 3 Html助手一起使用。我有一些深度嵌套的ViewModel和一些HtmlHelper扩展来帮助处理这些,所以表达式只需要MemberExpressions的集合,用于内置的MVC帮助程序正确处理它们并构建正确深度嵌套的名称属性值。我的第一直觉是使用Expression.Invoke()并调用第一个表达式并将其链接到第二个表达式,但MVC助手并不那么喜欢。它失去了等级背景。

3 个答案:

答案 0 :(得分:21)

使用访问者将参数f的所有实例交换为m.SubModel.Foo,并创建一个以m为参数的新表达式:

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

答案 1 :(得分:6)

您的解决方案似乎针对您的具体问题进行了狭窄的调整,这似乎不灵活。

在我看来,你可以通过简单的lambda替换直接解决你的问题:用body取代参数的实例(或者在lambda演算中称为“自由变量”)。 (请参阅Marc对某些代码的回答。)

由于表达式树中的参数具有参照标识而不是值标识,因此甚至不需要对其进行重命名。

也就是说,你有:

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

并且您希望生成组合

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

然后选择正文g(b),搜索并替换访问者,为ParameterExpression寻找b,并将其替换为正文f(a)给您新身体g(f(a))。然后使用具有该主体的参数a创建一个新的lambda。

答案 2 :(得分:0)

更新:以下答案会生成EF不支持的“调用”。

我知道这是一个老线程,但我有同样的需求,我想出了一个更清洁的方法来做到这一点。假设您可以将“expression2”更改为使用通用lambda,您可以注入如下:

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