none
Combine 2 expressions using Expression API RRS feed

  • Question

  • I'm trying to combine map & filter expressions into one

    public static Expression<Func<T, bool>> Combine<T, U>(this Expression<Func<T, U>> map,
                Expression<Func<U, bool>> filter)

    If I would have the same with Func's I would have

    public static Func<T, bool> Combine<T, U>(this Func<T, U> map,
                Func<U, bool> filter) => input => filter(map(input));

    I'm trying to construct the same via C# expressions API, but can't make it work. Here's how far I got

    public static Expression<Func<T, bool>> Combine<T, U>(this Expression<Func<T, U>> map,
        Expression<Func<U, bool>> filter)
    {
        ParameterExpression tVariableExpression = Expression.Variable(typeof(U), "u");
        Expression converter = Expression.Assign(tVariableExpression, map.Body);
    
        ParameterExpression resultExpression = Expression.Variable(typeof(bool), "result");
        Expression predicate = Expression.Assign(resultExpression, filter.Body);
    
        var block = Expression.Block(
            new[] { tVariableExpression, resultExpression },
            converter, predicate);
    
        return Expression.Lambda<Func<T, bool>>(block, map.Parameters);
    }

    And here's the usage

    Expression<Func<string, int>> stringToInt = text => text.Length;
    Expression<Func<int, bool>> lengthToBool = length => length % 2 == 0;
    
    var combined = stringToInt.Combine(lengthToBool);
    
    var compiledCombined = combined.Compile();
    
    var odd = compiledCombined.Invoke("Foo");

    If I run this, I get the following error when compiling lambda.

    System.InvalidOperationException: 'variable 'length' of type 'System.Int32' referenced from scope '', but it is not defined'

    What am I'm doing wrong here ? How can I make this work ?


    • Edited by _gh_manvel_ Wednesday, July 17, 2019 4:09 PM More meaningful title
    Wednesday, July 17, 2019 4:09 PM

Answers

  • Try this:

    public static Expression<Func<T, bool>> Combine<T, U>( this Expression<Func<T, U>> map, Expression<Func<U, bool>> filter )
    {
        //  input => filter( map( input ) );
    
        var input = Expression.Variable( typeof( T ), "input" );
        var invoke1 = Expression.Invoke( map, input );
        var invoke2 = Expression.Invoke( filter, invoke1 );
    
        return Expression.Lambda<Func<T, bool>>( invoke2, input );
    }
    

    • Marked as answer by _gh_manvel_ Thursday, July 18, 2019 1:55 PM
    Wednesday, July 17, 2019 6:56 PM

All replies

  • Try this:

    public static Expression<Func<T, bool>> Combine<T, U>( this Expression<Func<T, U>> map, Expression<Func<U, bool>> filter )
    {
        //  input => filter( map( input ) );
    
        var input = Expression.Variable( typeof( T ), "input" );
        var invoke1 = Expression.Invoke( map, input );
        var invoke2 = Expression.Invoke( filter, invoke1 );
    
        return Expression.Lambda<Func<T, bool>>( invoke2, input );
    }
    

    • Marked as answer by _gh_manvel_ Thursday, July 18, 2019 1:55 PM
    Wednesday, July 17, 2019 6:56 PM
  • Thank you for reply. I was trying to get it w/o invoking Expressions as this is going to be used with EF & invoking expressions are causing issues.


    Wednesday, July 17, 2019 8:21 PM
  • Hi _gh_manvel_, 

    Thank you for posting here.

    For your question, you want to combine multiple expressions into one.

    I have made a sample on my side, and you can refer it.

    Here’s the code:

        public class ReplaceVisitor : ExpressionVisitor
        {
            Expression _left;
            Expression _right;
            public ReplaceVisitor(Expression left, Expression right)
            {
                _left = left;
                _right = right;
            }
            public override Expression Visit(Expression node)
            {
                if (node.Equals(_left))
                {
                    return _right;
                }
    
                return base.Visit(node);
            }
        }
        class Program
        {
            
            static void Main(string[] args)
            {
                Expression<Func<string, int>> stringToInt = text => text.Length;
                Expression<Func<int, bool>> lengthToBool = length => length % 2 == 0;
    
                var expressionBody = Replace(lengthToBool.Body, lengthToBool.Parameters[0], stringToInt.Body);
                var resultExpression = Expression.Lambda<Func<string, bool>>(expressionBody, stringToInt.Parameters[0]);
                var func = resultExpression.Compile();
                Console.WriteLine(func("1234"));
                Console.ReadKey();
            }
            public static Expression Replace(Expression main, Expression current, Expression replacement)
            {
                return (new ReplaceVisitor(current, replacement)).Visit(main);
            }
        }

    Result of the test:



    Hope it can help you.

    Best Regards,

    Xingyu Zhao


    MSDN Community Support
    Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    Thursday, July 18, 2019 6:08 AM
    Moderator
  • Thank you for response.

    I see you replaced content of 2nd lambda with the first. So length % 2 -> became -> text.Length % 2.

    Note that my goal is to create general purpose Combine, so I don't know what those 2 lambdas are going to be.

    I'm questioning if doing replace is safe here, since we don't know what's the first lambda. What if it's operation has less precedence, e.g.

    firstLambda = x => 7 -2

    secondLambda = y => x % 2 == 0

    combinedWithReplace = x => (7 - 2 % 2) == 0

    Which is wrong, it supposed to calc. 7 - 2 first, and then use result (e.g. 5) to check if 5 % 2 == 0. In this case it will do 2 % 2 and then 7 - 0 == 0.

    Also as I said my goal was to have general purpose Combine, if I take your code and replace my Combine method

    public static Expression<Func<T, bool>> Combine<T, U>(this Expression<Func<T, U>> map,
        Expression<Func<U, bool>> filter)
    {
        var expressionBody = Replace(map.Body, filter.Parameters[0], map.Body);
        var resultExpression = Expression.Lambda<Func<T, bool>>(expressionBody, filter.Parameters[0]);
    
        return resultExpression;
    }
    
    public static Expression Replace(Expression main, Expression current, Expression replacement)
    {
        return (new ReplaceVisitor(current, replacement)).Visit(main);
    }
    

    It fails to run saying parameter type can't be an int32.

    Thursday, July 18, 2019 1:22 PM