Combine two Linq lambda expressions

后端 未结 1 899
日久生厌
日久生厌 2020-11-27 19:45
Expression> fn1 = x => x.PossibleSubPath.MyStringProperty;

Expression> fn2 = x => x.Contains         


        
相关标签:
1条回答
  • 2020-11-27 20:12

    So logically what we want to be able to do is create a new lambda in which it has a parameter of the input to the first function, and a body that calls the first function with that parameter and then passes the result as the parameter to the second function, and then returns that.

    We can replicate that easily enough using Expression objects:

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

    Sadly, EF and most other query providers won't really know what to do with that and won't function properly. Whenever they hit an Invoke expression they generally just throw an exception of some sort. Some can handle it though. In theory all the information they need is there, if they're written with the robustness to get at it.

    What we can do however is, from a conceptual standpoint, replace every instance of the first lambda's parameter in that lambda's body with the parameter of a new lambda we're creating, and then replace all instances of the second lambda's parameter in the second lambda with the new body of the first lambda. Technically, if these expressions have side effects, and these parameters are used more than once, they wouldn't be the same, but as these are going to be parsed by an EF query provider they really shouldn't ever have side effects.

    Thanks to David B for providing a link to this related question which provides a ReplaceVisitor implementation. We can use that ReplaceVisitor to go through the entire tree of an expression and replace one expression with another. The implementation of that type is:

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

    And now we can write our proper Combine method:

    public static Expression<Func<T1, T3>> Combine<T1, T2, T3>(
        this 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);
    }
    

    and a simple test case, to just demonstrate what's going on:

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

    Which will print out:

    param => param.PossibleSubPath.MyStringProperty.Contains("some literal")

    Which is exactly what we want; a query provider will know how to parse something like that.

    0 讨论(0)
提交回复
热议问题