简体   繁体   中英

Convert x => array.Contains(x) expression into x=> x == 1 || x ==2

Assuming that array is an array of integers:

var array = new [] { 1, 2 }

And let's say there is an object name Some , with properties:

public class Some
{
    public int Id { get; set;}
}

I need a way to convert:

Expression<Func<Some, bool>> exp = x => array.Contains(x.Id)

expression into:

Expression<Func<Some, bool>> exp = x => x.Id == 1 || x.Id == 2

UPDATE

I already have an extension method on list which generates wanted result from a list: What I am asking is, given expression 1 how could I convert it to expression 2. I don't want to push other team members to use extension instead of normal contains method.

my extension method:

array.SafeContainsExpression<Some, string>(nameof(Some.Id));

and the code:

    public static Expression<Func<TModel, bool>> SafeContainsExpression<TModel, TValue>(
        this IEnumerable<TValue> list, string propertyName)
    {
        var argParam = Expression.Parameter(typeof(TModel), "x");
        var selector = Expression.Property(argParam, propertyName);

        Expression left = null;
        foreach (var value in list)
        { 
            var valueExpression = Expression.Constant(value, typeof(TValue));
            var right = Expression.Equal(selector, valueExpression);

            if (left == null)
                left = right;

            left = Expression.OrElse(left, right);
        }

        return Expression.Lambda<Func<TModel, bool>>(left, argParam);
    }

SOLUTION (From accepted answer)

public class SafeExpressionsVisitor : LinqKit.ExpressionVisitor
{
    protected override Expression VisitMethodCall(MethodCallExpression m)
    {
        if (m.Method.Name == "Contains" && m.Arguments.Count == 2)
        {
            var list = Expression.Lambda<Func<IEnumerable>>(m.Arguments[0]).Compile()();
            var propertyExpression = (MemberExpression)m.Arguments[1];
            Expression left = null;
            foreach (var value in list)
            {
                var valueExpression = Expression.Constant(value);
                var right = Expression.Equal(propertyExpression, valueExpression);

                if (left == null)
                {
                    left = right;
                    continue;
                }

                left = Expression.OrElse(left, right);
            }

            return left;
        }
        return base.VisitMethodCall(m);
    }
}


public class ExpressionTests
{
    [Fact]
    public void Shoul_Convert_With_Visitor()
    {
        var array = new[] { 1, 2 };

        Expression<Func<A, bool>> exp = x => array.Contains(x.Id);

        var safeExp = Expression.Lambda<Func<A, bool>>(
            new SafeExpressionsVisitor().Visit(exp.Body),
            exp.Parameters);

        var func = safeExp.Compile();

        Assert.True(func(new A { Id = 1 }));
        Assert.True(func(new A { Id = 2 }));
        Assert.False(func(new A { Id = 3 }));
    }
}

The conversion can work you can build an expression like so.

 Expression<Func<A, bool>> exp2 = Expression.Lambda<Func<A, bool>>(
                array.Select(i=>Expression.Equal(Expression.Property(p1,"Id"),Expression.Constant(i))).Aggregate((a,i)=> a == null? i:Expression.OrElse(a,i)),p1);

EDIT This will convert the example but for more generic stuff you need to cover more cases of Expression tree.

var array = new[] { 1, 2 };

            Expression<Func<A, bool>> exp = x => array.Contains(x.Id);
            Expression<Func<A, bool>> exp2 = x => x.Id == 1 || x.Id == 2;


            var p1 = Expression.Parameter(typeof(A));

            var exp3 = Expression.Lambda<Func<A, bool>>(ExpressionVisitor.Visit(new []{ exp.Body }.ToList().AsReadOnly(), (m) => {
                if (m.NodeType == ExpressionType.Call)
                {
                    var method = (MethodCallExpression)m;
                    if (method.Method.Name == "Contains" && method.Arguments.Count == 2)
                    {
                        var items = Expression.Lambda<Func<object>>(method.Arguments[0]).Compile()();
                        var prop = ((MemberExpression)method.Arguments[1]);
                        return ((IEnumerable<int>)items).Select(i => Expression.Equal(Expression.Property(prop.Expression, prop.Member.Name), Expression.Constant(i))).Aggregate((a, i) => a == null ? i : Expression.OrElse(a, i));

                    }
                }
                return m;
            })[0],exp.Parameters);


            var func = exp3.Compile();

            Console.WriteLine(func(new A { Id = 1 }));
            Console.WriteLine(func(new A { Id = 2 }));
            Console.WriteLine(func(new A { Id = 3 }));

Instead of

Expression<Func<Some, bool>> exp = x => array.Contains(x.Id)

You can write:

var filters = array.Select<int, Expression<Func<Some, bool>>>(i =>
  x => x.Id == i);

Expression<Func<Some, bool>> exp = filters.OrTheseFiltersTogether();

Using my classic method:

public static Expression<Func<T, bool>> OrTheseFiltersTogether<T>(
  this IEnumerable<Expression<Func<T, bool>>> filters)
{
    Expression<Func<T, bool>> firstFilter = filters.FirstOrDefault();
    if (firstFilter == null)
    {
        Expression<Func<T, bool>> alwaysTrue = x => true;
        return alwaysTrue;
    }

    var body = firstFilter.Body;
    var param = firstFilter.Parameters.ToArray();
    foreach (var nextFilter in filters.Skip(1))
    {
        var nextBody = Expression.Invoke(nextFilter, param);
        body = Expression.OrElse(body, nextBody);
    }
    Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>(body, param);
    return result;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM