1

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 }));
    }
}
  • What do you mean by convert? Why not replace? – Sergey Berezovskiy Jun 16 '17 at 12:50
  • 4
    Why, though? If this is some kind of optimization pass, you'd be better off replacing it with `HashSet.Contains`. – Jeroen Mostert Jun 16 '17 at 12:52
  • @SergeyBerezovskiy this is a pseudo code, value of array is changed at run time. I think that I need an `ExpressionVisitor` that will get value of the array and for each value create expression from `Expression.Equal` and chain them with `Expression.OrElse`. but I can't figure out how to do it. – Merdan Gochmuradov Jun 16 '17 at 12:57
  • 2
    @JeroenMostert I want this to query RavenDB that doesn't support `contains` method – Merdan Gochmuradov Jun 16 '17 at 12:59
  • 7
    Is https://stackoverflow.com/a/7902851/34092 an option? – mjwills Jun 16 '17 at 13:00
  • Fair enough. Be careful, though, that the resulting expression tree is linear in the size of the array, which can easily cause problems evaluating it if you're testing for many values. I don't know if RavenDB has an efficient `IN` operator you could use (or something comparable to it). – Jeroen Mostert Jun 16 '17 at 13:00
  • @mjwills actually I am using Repository pattern, and client where I am calling this method doesn't knows anything about RavenDB special methods, but: `Contains` operator to `IN` operator converter also could work as a solution. – Merdan Gochmuradov Jun 16 '17 at 13:09
  • https://stackoverflow.com/questions/5397777/implementing-the-repository-and-service-pattern-with-ravendb may be worth a read. – mjwills Jun 16 '17 at 13:11
  • And that is why Repository pattern is the dumbest thing ever. NOTE In Repository pattern you should not expose IQueryable. – Filip Cordas Jun 16 '17 at 13:23
  • When you use a Repository then that is the place to solve this, and where you _do_ know about your type of Db. – H H Jun 16 '17 at 13:59
  • @MerdanGochmuradov I made an edit on my answer but not sure if it is as generic as you would like. – Filip Cordas Jun 16 '17 at 14:51
  • @HenkHolterman I thing he is not really using Repository pattern but one of many IQueriable Get(); or similar implementations. This is not really I Repository but a lot of people call it that. – Filip Cordas Jun 16 '17 at 14:54

2 Answers2

1

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 }));
Filip Cordas
  • 2,531
  • 1
  • 12
  • 23
  • Thanks for the answer, I have made an ExpressionVisitor based on your answer, work like a charm )), will update my question to show the solution. – Merdan Gochmuradov Jun 16 '17 at 17:51
  • @MerdanGochmuradov Just one note be there might be some edge cases witch are not covered so do a lot of testing. – Filip Cordas Jun 16 '17 at 17:58
1

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;
}
Amy B
  • 108,202
  • 21
  • 135
  • 185
  • Very few query providers actually support `Invoke`. Almost all just throw when they come across it. – Servy Jun 16 '17 at 14:40