【问题标题】:Convert x => array.Contains(x) expression into x=> x == 1 || x ==2将 x => array.Contains(x) 表达式转换为 x=> x == 1 || x ==2
【发布时间】:2017-06-16 12:49:14
【问题描述】:

假设该数组是一个整数数组:

var array = new [] { 1, 2 }

假设有一个对象名称​​Some,带有属性:

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

我需要一种转换方式:

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

表达式为:

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

更新

我已经在列表上有一个扩展方法,它从列表中生成想要的结果:我要问的是,给定表达式 1,我如何将其转换为表达式 2。我不想推动其他团队成员使用扩展而不是普通的 contains 方法。

我的扩展方法:

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

和代码:

    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);
    }

解决方案(来自已接受的答案)

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 }));
    }
}

【问题讨论】:

  • 转换是什么意思?为什么不换?
  • 为什么?如果这是某种优化传递,最好用HashSet.Contains 替换它。
  • @SergeyBerezovskiy 这是一个伪代码,数组的值在运行时改变。我认为我需要一个ExpressionVisitor 来获取数组的值,并为每个值从Expression.Equal 创建表达式并将它们与Expression.OrElse 链接起来。但我不知道该怎么做。
  • @JeroenMostert 我想让它查询不支持 contains 方法的 RavenDB
  • stackoverflow.com/a/7902851/34092 是一个选项吗?

标签: c# linq expression


【解决方案1】:

转换可以工作,您可以像这样构建表达式。

 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);

编辑 这将转换示例,但对于更通用的内容,您需要涵盖更多表达式树的情况。

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 }));

【讨论】:

  • 感谢您的回答,我已经根据您的回答制作了一个 ExpressionVisitor,就像一个魅力 )),将更新我的问题以显示解决方案。
  • @MerdanGochmuradov 请注意,可能存在一些未涵盖的边缘情况,因此请进行大量测试。
【解决方案2】:

代替

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

你可以写:

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

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

使用我的经典方法:

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;
}

【讨论】:

  • 很少有查询提供者真正支持Invoke。几乎所有人在遇到它时都会扔掉。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-08
  • 1970-01-01
  • 2022-06-10
  • 1970-01-01
  • 2016-09-13
相关资源
最近更新 更多