Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combining expressions c#

I need to concatenate two expressions (with or statement)

My code:

var items = new List<Item>
{
    new Item { Color = "Black", Categories = new List<string> { "cat1", "cat2" } },
    new Item { Color = "Red", Categories = new List<string> { "cat3" } },
    new Item { Color = "White", Categories = new List<string> { "cat1" } }
};

var categories = new List<string> { "cat2", "cat3" };

Expression<Func<Item, bool>> func1 = (x1) => x1.Color == "Black";
Expression<Func<Item, bool>> func2 = (x2) => x2.Categories.Any(y => categories.Where(z => z == y).Any());
Expression<Func<Item, bool>> fullExpression = Expression.Lambda<Func<Item, bool>>(
        Expression.Or(func1.Body, func2.Body), func1.Parameters.Single());

var result = items.AsQueryable().Where(fullExpression);
// result should be like this
// items.Where(x => (x.Color == "Black") || x.Categories.Any(y => categories.Where(z => z == y).Any()))

I get run-time error variable 'x2' of type 'Item' referenced from scope '', but it is not defined'

I also was trying to build an expression with ExpressionVisitor.

Here is ExpressionVisitor:

internal class ParameterReplacer : ExpressionVisitor
{
    private readonly ParameterExpression _parameter;

    internal ParameterReplacer(ParameterExpression parameter)
    {
        _parameter = parameter;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return base.VisitParameter(_parameter);
    }
}

Here how I use it in code:

Expression<Func<Item, bool>> func1 = (x1) => x1.Color == "Black";
Expression<Func<Item, bool>> func2 = (x2) => x2.Categories.Any(y => categories.Select(z => z == y).Any());
var paramExpr = Expression.Parameter(typeof(Item));
var exprBody = Expression.Or(func1.Body, func2.Body);
exprBody = (BinaryExpression)new ParameterReplacer(paramExpr).Visit(exprBody);
var finalExpr = Expression.Lambda<Func<Item, bool>>(exprBody, paramExpr);
var result = items.AsQueryable().Where(finalExpr);

In this case during creating ParameterReplacer I'm getting error

System.InvalidOperationException: 'The operands for operator 'Equal' do not match the parameters of method 'op_Equality'.'

What did I do wrong?

like image 853
Pyrejkee Avatar asked Jan 25 '23 18:01

Pyrejkee


2 Answers

Ian Newson is entirely right but if you want code, here you go :)

Using these two classes you can combine the two predicates. I didn't come up with it but improved/adjusted it a bit and made it use the type Predicate instead of Func along with some newer language features (the original was quite old, sadly I don't remember where I found it).

internal class SubstExpressionVisitor : ExpressionVisitor
{
    private readonly Dictionary<Expression, Expression> _subst = new Dictionary<Expression, Expression>();

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (_subst.TryGetValue(node, out Expression newValue))
        {
            return newValue;
        }

        return node;
    }

    public Expression this[Expression original]
    {
        get => _subst[original];
        set => _subst[original] = value;
    }
}
public static class PredicateBuilder
{
    // you don't seem to need this but it's included for completeness sake
    public static Expression<Predicate<T>> And<T>(this Expression<Predicate<T>> a, Expression<Predicate<T>> b)
    {
        if (a == null)
            throw new ArgumentNullException(nameof(a));

        if (b == null)
            throw new ArgumentNullException(nameof(b));

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor[b.Parameters[0]] = p;

        Expression body = Expression.AndAlso(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Predicate<T>>(body, p);
    }

    public static Expression<Predicate<T>> Or<T>(this Expression<Predicate<T>> a, Expression<Predicate<T>> b)
    {
        if (a == null)
            throw new ArgumentNullException(nameof(a));

        if (b == null)
            throw new ArgumentNullException(nameof(b));

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor[b.Parameters[0]] = p;

        Expression body = Expression.OrElse(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Predicate<T>>(body, p);
    }
}

You can use it like this:

Expression<Predicate<Item>> func1 = (x1) => x1.Color == "Black";
Expression<Predicate<Item>> func2 = (x2) => x2.Categories.Any(y => categories.Select(z => z == y).Any());

Expression<Predicate<Item>> finalExpr = func1.Or(func2);

You might want to keep in mind that my Or is using OrElse internally so the second expression won't be evaluated if the one before is evaluated to true (OrElse is like ||, not |). The same goes for And with AndAlso (AndAlso is like &&, not &).
Another thing to note is that you can easily replace Predicate<T> with Func<T, bool> if you have to use Func for some reason :)

like image 183
Joelius Avatar answered Jan 28 '23 07:01

Joelius


This is because your two expressions (func1 and func2) reference two different ParameterExpressions. Just because they're of the same type doesn't mean they're the same.

They need to be the exact same ParameterExpression instance for this to work. For that you can leverage an expression rewriter to modify one of the expressions: https://learn.microsoft.com/en-us/dotnet/api/system.linq.expressions.expressionvisitor?view=netframework-4.8

I think you should be able to use a library like predicate builder to do the same thing in a simpler way though:

https://www.nuget.org/packages/PredicateBuilder/

EDIT:

Your code is on the right lines, but make the following changes:

For the visitor:

internal class ParameterReplacer : ExpressionVisitor
{
    private readonly ParameterExpression _parameter;

    internal ParameterReplacer(ParameterExpression parameter)
    {
        _parameter = parameter;
    }

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

For the execution bit:

        Expression<Func<Item, bool>> func1 = (x1) => x1.Color == "Black";
        Expression<Func<Item, bool>> func2 = (x2) => x2.Categories.Any(y => categories.Select(z => z == y).Any());
        var paramExpr = func1.Parameters.Single();
        var expr2 = new ParameterReplacer(paramExpr).Visit(func1);
        var exprBody = Expression.Or(func1.Body, ((LambdaExpression)expr2).Body);

        var finalExpr = Expression.Lambda<Func<Item, bool>>(exprBody, paramExpr);

        var result = items.AsQueryable().Where(finalExpr)
            .ToList();
like image 20
Ian Newson Avatar answered Jan 28 '23 07:01

Ian Newson