基于表达式树构建的ORing LINQ查询

3

所以,这有点复杂。

我有一组规则存储在一个集合里,一个规则包含以下三个属性。

Field, Op, and Data (all strings)

因此,一条规则可能看起来像是“State”,“eq”,“CA”。

我的一般规则是所有规则都要一起使用。但是,如果它们具有相同的字段值,则所有规则都应该使用OR一起使用。这使我们能够说“State”,“eq”,“CA”,或“State”,“eq”,“TX”并且“FirstName”,“eq”,“John”。

问题是,我的当前应用规则的方式不起作用,因为它仅仅使用每个规则来构建linq表达式,导致越来越明确。

var result = rules.Aggregate(_repository.All, (current, rule) => current.ExtendQuery(rule))
ExtendQuery是我编写的扩展方法,它使用表达式树来生成一个新查询,将当前规则应用于传入查询。(实际上将它们全部连接在一起)。
现在,我可以很容易地修改.Aggregate行,按字段分组规则,然后为每个字段生成唯一查询,但是如何让它们"OR"而不是"AND"呢?
然后,对于这些查询中的每一个,我该如何使它们"AND"在一起呢?使用联合查询吗? ExtendQuery看起来像这样。
    public static IQueryable<T> ExtendQuery<T>(this IQueryable<T> query, QueryableRequestMessage.WhereClause.Rule rule) where T : class
    {
        var parameter = Expression.Parameter(typeof(T), "x");
        Expression property = Expression.Property(parameter, rule.Field);
        var type = property.Type;

        ConstantExpression constant;
        if (type.IsEnum)
        {
            var enumeration = Enum.Parse(type, rule.Data);
            var intValue = (int)enumeration;
            constant = Expression.Constant(intValue);
            type = typeof(int);
            //Add "Id" by convention, this is all because enum support is lacking at this point in Entity Framework
            property = Expression.Property(parameter, rule.Field + "Id");
        }
        else if(type == typeof(DateTime))
        {
            constant = Expression.Constant(DateTime.ParseExact(rule.Data, "dd/MM/yyyy", CultureInfo.CurrentCulture));
        }
        else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            //This will convert rule.Data to the baseType, not a nullable type (because that won't work)
            var converter = TypeDescriptor.GetConverter(type);
            var value = converter.ConvertFrom(rule.Data);
            constant = Expression.Constant(value);

            //We change the type of property to get converted to it's base type
            //This is because Expression.GreaterThanOrEqual can't compare a decimal with a Nullable<decimal>
            var baseType = type.GetTypeOfNullable();
            property = Expression.Convert(property, baseType);
        }
        else
        {
            constant = Expression.Constant(Convert.ChangeType(rule.Data, type));
        }

        switch (rule.Op)
        {
            case "eq": //Equals
            case "ne": //NotEquals
                {
                    var condition = rule.Op.Equals("eq")
                                        ? Expression.Equal(property, constant)
                                        : Expression.NotEqual(property, constant);
                    var lambda = Expression.Lambda(condition, parameter);
                    var call = Expression.Call(typeof(Queryable), "Where", new[] { query.ElementType }, query.Expression, lambda);
                    query = query.Provider.CreateQuery<T>(call);
                    break;
                }
            case "lt": //Less Than
                    query = type == typeof (String) 
                        ? QueryExpressionString(query, Expression.LessThan, type, property, constant, parameter)
                        : QueryExpression(query, Expression.LessThan, property, constant, parameter); break;
            case "le": //Less Than or Equal To
                query = type == typeof (String)
                        ? QueryExpressionString(query, Expression.LessThanOrEqual, type, property, constant, parameter)
                        : QueryExpression(query, Expression.LessThanOrEqual, property, constant, parameter); break;
            case "gt": //Greater Than
                query = type == typeof (String)
                        ? QueryExpressionString(query, Expression.GreaterThan, type, property, constant, parameter)
                        : QueryExpression(query, Expression.GreaterThan, property, constant, parameter); break;
            case "ge": //Greater Than or Equal To 
                query = type == typeof (String)
                        ? QueryExpressionString(query, Expression.GreaterThanOrEqual, type, property, constant, parameter)
                        : QueryExpression(query, Expression.GreaterThanOrEqual, property, constant, parameter); break;
            case "bw": //Begins With
            case "bn": //Does Not Begin With
                query = QueryMethod(query, rule, type, "StartsWith", property, constant, "bw", parameter); break;
            case "ew": //Ends With
            case "en": //Does Not End With
                query = QueryMethod(query, rule, type, "EndsWith", property, constant, "cn", parameter); break;
            case "cn": //Contains
            case "nc": //Does Not Contain
                query = QueryMethod(query, rule, type, "Contains", property, constant, "cn", parameter); break;
            case "nu": //TODO: Null
            case "nn": //TODO: Not Null
                break;
        }

        return query;
    }

    private static IQueryable<T> QueryExpression<T>(
        IQueryable<T> query,
        Func<Expression, Expression, BinaryExpression> expression,
        Expression property,
        Expression value,
        ParameterExpression parameter
    ) where T : class
    {
        var condition = expression(property, value);
        var lambda = Expression.Lambda(condition, parameter);
        var call = Expression.Call(typeof(Queryable), "Where", new[] { query.ElementType }, query.Expression, lambda);
        query = query.Provider.CreateQuery<T>(call);
        return query;
    }

    private static IQueryable<T> QueryExpressionString<T>(
        IQueryable<T> query,
        Func<Expression, Expression, BinaryExpression> expression,
        Type type, 
        Expression property, 
        Expression value, 
        ParameterExpression parameter)
    {
        var containsmethod = type.GetMethod("CompareTo", new[] { type });
        var callContains = Expression.Call(property, containsmethod, value);
        var call = expression(callContains, Expression.Constant(0, typeof(int)));
        return query.Where(Expression.Lambda<Func<T, bool>>(call, parameter));
    }

    private static IQueryable<T> QueryMethod<T>(
        IQueryable<T> query,
        QueryableRequestMessage.WhereClause.Rule rule,
        Type type,
        string methodName,
        Expression property,
        Expression value,
        string op,
        ParameterExpression parameter
    ) where T : class
    {
        var containsmethod = type.GetMethod(methodName, new[] { type });
        var call = Expression.Call(property, containsmethod, value);
        var expression = rule.Op.Equals(op)
                             ? Expression.Lambda<Func<T, bool>>(call, parameter)
                             : Expression.Lambda<Func<T, bool>>(Expression.IsFalse(call), parameter);
        query = query.Where(expression);
        return query;
    }

你考虑过在字段名称上使用分组,然后检查是否存在多个来使用“OR”吗?如果组中只有一个存在,则正常处理? - Sam
但我不确定如何编写“OR”。 - CaffGeek
3个回答

1
你可以使用 PredicateBuilder from LINQKit 来实现这一点。如果为每个规则创建一个 Expression,则可以使用 And()Or() 方法来组合它们(可能使用 PredicateBuilder.True()False() 作为基本情况)。最后,调用 Expand(),以便查询的形式可以被查询提供程序理解。
假设您将 ExtendQuery() 更改为返回一个 Expression 并将其重命名为 CreateRuleQuery(),则您的代码可能如下所示:
static IQueryable<T> ApplyRules<T>(
    this IQueryable<T> source, IEnumerable<Rule> rules)
{
    var predicate = PredicateBuilder.True<T>();

    var groups = rules.GroupBy(r => r.Field);

    foreach (var group in groups)
    {
        var groupPredicate = PredicateBuilder.False<T>();

        foreach (var rule in group)
        {
            groupPredicate = groupPredicate.Or(CreateRuleQuery(rule));
        }

        predicate = predicate.And(groupPredicate);
    }

    return source.Where(predicate.Expand());
}

使用方法类似于:

IQueryable<Person> source = …;

IQueryable<Person> result = source.ApplyRules(rules);

如果你在这些规则上使用了这个:

Name, eq, Peter
Name, eq, Paul
Age, ge, 18

那么谓词体将是(从谓词的调试视图):

True && (False || $f.Name == "Peter" || $f.Name == "Paul") && (False || $f.Age >= 18)

所有的TrueFalse都不应该是问题,但你可以通过使ApplyRules()变得更加复杂来摆脱它们。

1
所以实际上很容易。
现在你的代码为每个规则生成一个 where,而你需要一个带有稍微复杂条件的 where,因此需要对你的代码进行一些修改。
private static Expression GetComparisonExpression(this Rule rule, ParameterExpression parameter)
    {
        Expression property = Expression.Property(parameter, rule.Field);
        ConstantExpression constant = Expression.Constant(4);

        /* the code that generates constant and does some other stuff */

        switch (rule.Op)
        {
            case "eq": //Equals
            case "ne": //NotEquals
                {
                    var condition = rule.Op.Equals("eq")
                                        ? Expression.Equal(property, constant)
                                        : Expression.NotEqual(property, constant);
                    return condition;
                }

            default:
                throw new NotImplementedException();
        }
    }

这是您原始代码所需的片段。此方法不会包装查询,而只会生成与给定参数中的任何内容进行比较表达式的语句。rule
现在从生成查询的语句开始:
 var result = rules.Generate(_repository.All);

生成方法按属性名称Field对您的规则进行分组,对于每个组,它会生成and also(这只是&&运算符)条件表达式:

(group1Comparision) && (group2Comparison) && so on


public static IQueryable<T> Generate<T>(this  IEnumerable<Rule> rules, IQueryable<T> query) where T : class
{
    if (rules.Count() == 0)
        return query;

    var groups = rules.GroupBy(x => x.Field).ToArray();

    var parameter = Expression.Parameter(typeof(T));
    var comparison = groups.First().GetComparisonForGroup(parameter);

    foreach (var group in groups.Skip(1))
    {
        var otherComparions = group.GetComparisonForGroup(parameter);
        comparison = Expression.AndAlso(comparison, otherComparions);
    }

    var lambda = Expression.Lambda(comparison, parameter);
    var call = Expression.Call(typeof(Queryable), "Where", new[] { query.ElementType }, query.Expression, lambda);
    return query.Provider.CreateQuery<T>(call);

}

请注意,按属性名称分组会使规则的原始顺序变得无关紧要。
最后一件事是为组创建比较,因此使用 || 运算符:
public static Expression GetComparisonForGroup(this IEnumerable<Rule> group, ParameterExpression parameter)
{
        var comparison = group.Select((rule) => rule.GetComparisonExpression(parameter)).ToArray();

        return comparison.Skip(1).Aggregate(comparison.First(),
            (left, right) => Expression.OrElse(left, right));
}

因此,对于给定的规则列表,不需要使用外部库:

var rules = new Rule[]
            {
                new Rule{ Field = "A", Data = "4", Op="ne"},
                new Rule{ Field = "B", Data = "4", Op="eq"},
                new Rule{ Field = "A", Data = "4", Op="eq"},
                new Rule{ Field = "C", Data = "4", Op="ne"},
                new Rule{ Field = "A", Data = "4", Op="eq"},
                new Rule{ Field = "C", Data = "4", Op="eq"},
            };

我生成了一个条件,将其引入到单个Where查询中:

($var1.A != 4 || $var1.A == 4 || $var1.A == 4) && $var1.B == 4 && ($var1.C != 4 || $var1.C == 4)

0
一个避免使用.AsExpandable()的LINQKit替代方案是下面的ExpressionBuilder类。它通过使用And或Or来组合两个Expression<Func<T,bool>>,创建一个新的表达式,并且使用纯Expression代码实现(没有.Compile()或类似的东西)。我记不清一些想法的原始来源了,如果有人知道,我很乐意添加。
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Collections.Generic;

public static class ExpressionBuilder
{
    public static Expression<Func<T, bool>> True<T>() { return f => true; }
    public static Expression<Func<T, bool>> False<T>() { return f => false; }

    public static Expression<T> Compose<T>(this Expression<T> first, 
         Expression<T> second, 
         Func<Expression, Expression, Expression> merge)
    {
        // build parameter map (from parameters of second to parameters of first)
        var map = first.Parameters
                     .Select((f, i) => new { f, s = second.Parameters[i] })
                     .ToDictionary(p => p.s, p => p.f);

        // replace parameters in the second lambda expression with parameters from 
        // the first
        var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);

        // apply composition of lambda expression bodies to parameters from 
        // the first expression 
        return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
    }

    public static Expression<Func<T, bool>> And<T>(
        this Expression<Func<T, bool>> first,
        Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.And);
    }

    public static Expression<Func<T, bool>> Or<T>(
        this Expression<Func<T, bool>> first,
        Expression<Func<T, bool>> second)
    {
        return first.Compose(second, Expression.Or);
    }

    public class ParameterRebinder : ExpressionVisitor
    {
        private readonly Dictionary<ParameterExpression, ParameterExpression> map;

        public ParameterRebinder(
            Dictionary<ParameterExpression, 
            ParameterExpression> map)
        {
            this.map = map??new Dictionary<ParameterExpression,ParameterExpression>();
        }

        public static Expression ReplaceParameters(
            Dictionary<ParameterExpression, 
            ParameterExpression> map, 
            Expression exp)
        {
            return new ParameterRebinder(map).Visit(exp);
        }

        protected override Expression VisitParameter(ParameterExpression p)
        {
            ParameterExpression replacement;
            if (map.TryGetValue(p, out replacement))
            {
                p = replacement;
            }
            return base.VisitParameter(p);
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接