How do I combine two Expressions?
Asked Answered
L

4

6

I am trying to build up an expression that will be applied to an IQueryable collection.

I can build an expression like this:

[TestClass]
public class ExpressionTests
{
    private IQueryable<MyEntity> entities;

    private class MyEntity
    {
        public string MyProperty { get; set; }
    }

    [TestInitialize]
    public void Setup()
    {
        entities = new[]
                    {
                        new MyEntity {MyProperty = "first"}, 
                        new MyEntity {MyProperty = "second"}
                    }.AsQueryable();
    }

    [TestMethod]
    public void TestQueryingUsingSingleExpression()
    {
        Expression<Func<MyEntity, bool>> expression = e => e.MyProperty.Contains("irs");
        Assert.AreEqual(1, entities.Where(expression).Count());
    }
}

Now I want to separate the two parts of the expression:

[TestMethod]
public void TestQueryingByCombiningTwoExpressions()
{
    Expression<Func<MyEntity, string>> fieldExpression = e => e.MyProperty;
    Expression<Func<string, bool>> operatorExpression = e => e.Contains("irs");
    // combine the two expressions somehow...
    Expression<Func<MyEntity, bool>> combinedExpression = ???;

    Assert.AreEqual(1, entities.Where(combinedExpression).Count());
}

Any suggestions as to how I might do this?

Btw the provider that will be resolving the expression is Linq for NHibernate.

Lawana answered 28/7, 2011 at 6:11 Comment(0)
J
7

Take a look at your two expression trees:

                 |                                      |
               Lambda                                Lambda
              /      \                              /      \
             /        \                            /        \
     Property          Parameter x               Call        Parameter y
    /        \                                  /  |  \
   /          \                                /   |   \
  x           MyProperty              EndsWidth    y    Constant
                                                        |
                                                       "5"

You need to create a new tree that looks like this:

                                 |
                               Lambda
                              /      \
                             /        \
                           Call        Parameter z
                          /  |  \
                         /   |   \
                   EndsWith  |   Constant
                             |         \
                          Property     "5"
                         /        \
                        /          \
                       z          MyProperty

You can easily see what parts of the new tree come from which original tree.

To create the tree, you take the body of the second lambda expression (Call) and replace all occurrences of y with the body of the first lambda expression (Property) and all occurrences of x with z. Then you wrap the result in a new lambda expression with parameter z.

You can use the ExpressionVisitor Class to rewrite the tree, and the Expression.Lambda Method to create the new lambda expression.

Jetliner answered 28/7, 2011 at 6:30 Comment(3)
Thanks dtb, great diagram, +1. I don't suppose you could supply an example that works on my sample code? I've tidied up my example code so that it can be run as a stand-alone unit test.Lawana
Sure. But I'd like to see you give it a try first :)Jetliner
Lol! I'm 2 hours into learning Expressions and I'm feeling like Alice down the rabbit hole. But you're right, I'll read the links you provided and see if I can work it out...Lawana
A
2

It depends on what the provider supports; if it supports sub-expressions (LINQ-to-SQL does, EF doesn't; I don't know about NH), then:

var combinedExpression = Expression.Lambda<Func<MyEntity, bool>>(
       Expression.Invoke(operatorExpression, fieldExpression.Body),
       fieldExpression.Parameters);

however, if it doesn't you'll need to use ExpressionVisitor to merge them.

Aside answered 28/7, 2011 at 6:31 Comment(3)
Thanks Marc, +1. Your answer works for an in-memory collection, but I should have mentioned that the provider I'm using is Linq for NHibernate and it doesn't work with that.Lawana
@Jonathan - you did mention that, which is why I said "I don't know about NH"; I have some ExpressionVisitor examples here on SO if it helps.Aside
As I recall LinqKit has some extension methods that let you use subexpressions with the entity framework and may also work with NH.Ljoka
L
2

Following the suggestion from dtb and Marc that I use the ExpressionVisitor to rewrite the expression tree, this was the cleanest I could manage:

public class ExpressionBuilder<T> : ExpressionVisitor where T : class
{
    private Expression fieldExpressionBody;

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return fieldExpressionBody;
    }

    public Expression<Func<T, bool>> Build(
        Expression<Func<T, string>> fieldExpression,
        Expression<Func<string, bool>> operatorExpression)
    {
        fieldExpressionBody = fieldExpression.Body;
        Expression newExpressionBody = Visit(operatorExpression.Body);
        return Expression.Lambda<Func<T, bool>>(newExpressionBody, fieldExpression.Parameters[0]);
    }
}

And using it in my unit test:

[TestMethod]
public void TestQueryingByCombiningTwoExpressions()
{
    Expression<Func<MyEntity, string>> fieldExpression = e => e.MyProperty;
    Expression<Func<string, bool>> operatorExpression = o => o.Contains("irs");

    var builder = new ExpressionBuilder<MyEntity>();
    Expression<Func<MyEntity, bool>> combinedExpression = builder.Build(fieldExpression, operatorExpression);
    Assert.AreEqual(1, entities.Where(combinedExpression).Count());
    Assert.AreEqual("e => e.MyProperty.Contains(\"irs\")", combinedExpression.ToString());
}
Lawana answered 1/8, 2011 at 23:17 Comment(0)
N
1

Out of completion, here is a version ready to work with 2, 3, n expressions

public class ExpressionMerger : ExpressionVisitor
{
    Expression CurrentParameterExpression { get; set; }

    public Expression<Func<TIn, TOut>> Merge<TIn, TA, TOut>(Expression<Func<TIn, TA>> inner, Expression<Func<TA, TOut>> outer)
    {
        return MergeAll<TIn, TOut>(inner, outer);
    }

    public Expression<Func<TIn, TOut>> Merge<TIn, TA, TB, TOut>(Expression<Func<TIn, TA>> inner, Expression<Func<TA, TB>> transition, Expression<Func<TB, TOut>> outer)
    {
        return MergeAll<TIn, TOut>(inner, transition, outer);
    }

    protected Expression<Func<TIn, TOut>> MergeAll<TIn, TOut>(params LambdaExpression[] expressions)
    {
        CurrentParameterExpression = expressions[0].Body;

        foreach (var expression in expressions.Skip(1))
        {
            CurrentParameterExpression = Visit(expression.Body);
        }

        return Expression.Lambda<Func<TIn, TOut>>(CurrentParameterExpression, expressions[0].Parameters[0]);
    } 

    protected override Expression VisitParameter(ParameterExpression node)
    {
        //replace current lambda parameter with ~previous lambdas
        return CurrentParameterExpression;
    }
}

}

Nemato answered 26/10, 2015 at 14:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.