2013-02-11 73 views
3

所以,這很複雜。ORing LINQ查詢,使用表達式樹構建

我在一個集合中有一組規則,一條規則包含這三個屬性。

Field, Op, and Data (all strings) 

所以規則可能看起來像「國家」,「EQ」,「CA」

我的一般規則是,所有的規則都與運算在一起。但是,有一點需要注意的是,如果它們具有相同的字段值,那麼所有的ORed。這允許我們說「狀態」,「eq」,「CA」或「狀態」,「eq」,「TX」和「FirstName」,「eq」,「John」。

問題是,我現在應用規則的方式不起作用,因爲它只是使用每個規則來構建linq表達式,以使其更加明確。

var result = rules.Aggregate(_repository.All, (current, rule) => current.ExtendQuery(rule)) 

ExtendQuery是一個擴展方法我寫的,它使用ExpressionTrees,生成一個新的查詢,適用目前的規則查詢傳遞。 (有效地使它們全部在一起)

現在,我不難修改.Aggregate這一行來按字段對規則進行分組,然後爲每個字段生成一個唯一查詢,但是如何獲取它到「或」他們在一起,而不是「和」?

然後對每個這些查詢,我將如何「與」他們在一起?聯盟?

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

您是否考慮過在字段名稱上使用分組,然後檢查是否存在多個使用「OR」?如果只有一個存在於組中,那麼正常處理? – Sam 2013-02-11 17:54:01

+0

但我不知道如何寫一個「或」 – CaffGeek 2013-02-11 17:57:35

回答

1

所以這實際上很容易。

現在你的代碼產生了:其中,每個規則,你需要的是一個地方有一點點複雜的條件,使一些修改你的代碼是爲了:

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

Th恩謂詞的身體會(從上游的調試視圖):

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

所有這些True S和False不是應該是一個問題,但你可以通過使ApplyRules()較爲複雜擺脫他們。

0

避免需要.AsExpandable()的LINQKit的替代方法是我的ExpressionBuilder類以下。它通過使用And或Or來組合兩個Expression<Func<T,bool>>,並創建一個新的表達式,並使用純Expression代碼(no .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); 
     } 
    } 
}