Combine two Linq lambda expressions
Asked Answered
T

1

19
Expression<Func<MyObject, string>> fn1 = x => x.PossibleSubPath.MyStringProperty;

Expression<Func<string, bool>> fn2 = x => x.Contains("some literal");

Is there a way to create a new lambda expression which basically uses the output of fn1 and uses it as input for fn2?

Expression<Func<MyObject, bool>> fnCombined = ...

I know that I can create the function at once, but the problem is that I'm making some generic code and therefore really need to be able to create these two functions separately, then combine them in such a way that Linq can use them on my database objects (Entity Framework).

Triquetrous answered 17/10, 2013 at 17:24 Comment(2)
Here's an answer showing expression manipulation/combination. https://mcmap.net/q/666764/-how-can-you-update-a-linq-expression-with-additional-parametersBullshit
@DavidB Thanks for the link; I was having trouble figuring out how to replace all instances of one expression with another.Galilee
G
30

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.

Galilee answered 17/10, 2013 at 18:11 Comment(3)
If fn2 uses its parameter more than once, fn1 will be repeated more than once. Hopefully this is either trivial or is handled intelligently by the query provider, but this could end up making significant work be done multiple times (something like a local variable ought to work, except query providers probably wouldn't support that).Airworthy
@TimS. Yeah, I discussed that a bit in my answer. To be honest, I don't expect it to be a major issue as I'd expect the query optimizer on the DB end to be able to effectively handle such cases. It also would appear that the kind of expressions the OP is looking into wouldn't be a problem. It's hard to say. A local variable almost certainly wouldn't be able to be properly parsed, as you mentioned.Galilee
@Servy, Can you update your answer to reflect, Passing the parameter from the first body, will not work because that parameter is defined in the scope of a seperate Expression, so when you combine your expression x => x.condition == true && x.anotherCondition == false the second x is not the same x in the first. (You can probably word what i'm trying to say better)Cyma

© 2022 - 2024 — McMap. All rights reserved.