2017-09-07 78 views
0

我有一个系统,允许将与销售有关的不同条件存储在数据库中。加载标准时,它们用于构建查询并返回所有适用的销售。该标准的对象是这样的:使用表达式树在循环中构建动态查询

ReferenceColumn(在他们申请销售表中的列)

MINVALUE(最低值的参考列必须)

的MaxValue(最大值参考列必须是)

搜索销售是使用上述标准的集合完成的。相同类型的ReferenceColumns一起进行OR运算,不同类型的ReferenceColumns一起进行AND运算。因此,举例来说,如果我有三个标准:

ReferenceColumn: '价格',MINVALUE: '10',MaxValue的: '20'

ReferenceColumn: '价格',MINVALUE: '80后',MaxValue的:“100 “

ReferenceColumn: '年龄',MINVALUE: '2',MaxValue的: '3'

查询应返回所有销售那里的价格是10-20之间或80-100之间,但所销售年龄在2至3岁之间。

我把它实现使用SQL查询字符串,并使用执行.FromSql:

public IEnumerable<Sale> GetByCriteria(ICollection<SaleCriteria> criteria) 
{ 
StringBuilder sb = new StringBuilder("SELECT * FROM Sale"); 

var referenceFields = criteria.GroupBy(c => c.ReferenceColumn); 

// Adding this at the start so we can always append " AND..." to each outer iteration 
if (referenceFields.Count() > 0) 
{ 
    sb.Append(" WHERE 1 = 1"); 
} 

// AND all iterations here together 
foreach (IGrouping<string, SaleCriteria> criteriaGrouping in referenceFields) 
{ 
    // So we can always use " OR..." 
    sb.Append(" AND (1 = 0"); 

    // OR all iterations here together 
    foreach (SaleCriteria sc in criteriaGrouping) 
    { 
     sb.Append($" OR {sc.ReferenceColumn} BETWEEN '{sc.MinValue}' AND '{sc.MaxValue}'"); 
    } 

    sb.Append(")"); 
} 

return _context.Sale.FromSql(sb.ToString(); 
} 

而这其实工作只是与我们的数据库很好,但它没有发挥好与其他收藏品,格外的InMemory数据库我们用于UnitTesting,所以我试图用表达式树重写它,这是我以前从未使用过的。到目前为止,我已经得到了这个:

public IEnumerable<Sale> GetByCriteria(ICollection<SaleCriteria> criteria) 
{ 
var referenceFields = criteria.GroupBy(c => c.ReferenceColumn); 

Expression masterExpression = Expression.Equal(Expression.Constant(1), Expression.Constant(1)); 
List<ParameterExpression> parameters = new List<ParameterExpression>(); 

// AND these... 
foreach (IGrouping<string, SaleCriteria> criteriaGrouping in referenceFields) 
{ 
    Expression innerExpression = Expression.Equal(Expression.Constant(1), Expression.Constant(0)); 
    ParameterExpression referenceColumn = Expression.Parameter(typeof(Decimal), criteriaGrouping.Key); 
    parameters.Add(referenceColumn); 

    // OR these... 
    foreach (SaleCriteria sc in criteriaGrouping) 
    { 
     Expression low = Expression.Constant(Decimal.Parse(sc.MinValue)); 
     Expression high = Expression.Constant(Decimal.Parse(sc.MaxValue)); 
     Expression rangeExpression = Expression.GreaterThanOrEqual(referenceColumn, low); 
     rangeExpression = Expression.AndAlso(rangeExpression, Expression.LessThanOrEqual(referenceColumn, high)); 
     innerExpression = Expression.OrElse(masterExpression, rangeExpression); 
    } 

    masterExpression = Expression.AndAlso(masterExpression, innerExpression); 
} 

var lamda = Expression.Lambda<Func<Sale, bool>>(masterExpression, parameters); 

return _context.Sale.Where(lamda.Compile()); 
} 

当我调用Expression.Lamda时,它目前正在抛出一个ArgumentException。 Decimal不能在那里使用,它表示它想要销售类型,但我不知道该在哪里销售,我不确定我在这里的正确轨道上。我也担心我的masterExpression每次都会自我复制,而不是像字符串构建器那样追加,但也许这样做会起作用。

我正在寻找关于如何将此动态查询转换为表达式树的帮助,如果我在此处建立基础,我可以采用完全不同的方法。

+0

剂量的原代码的工作?这不应该工作,为什么你使用1 = 1和1 = 0? –

+0

是的,如果集合是使用SQL Server的DbContext的一部分,它就可以工作。 1 = 1和1 = 0,所以我总是可以追加'AND'/'OR'到查询字符串,而不必处理第一次迭代特例等。 – Valuator

+0

尝试使用LINQKit(http://www.albahari .com/nutshell/linqkit.aspx),它使它更容易。该页面说:使用LINQKit,您可以:...动态构建谓词 – Tom

回答

1

我认为这会为你工作

public class Sale 
      { 
       public int A { get; set; } 

       public int B { get; set; } 

       public int C { get; set; } 
      } 

      //I used a similar condition structure but my guess is you simplified the code to show in example anyway 
      public class Condition 
      { 
       public string ColumnName { get; set; } 

       public ConditionType Type { get; set; } 

       public object[] Values { get; set; } 

       public enum ConditionType 
       { 
        Range 
       } 

       //This method creates the expression for the query 
       public static Expression<Func<T, bool>> CreateExpression<T>(IEnumerable<Condition> query) 
       { 
        var groups = query.GroupBy(c => c.ColumnName); 

        Expression exp = null; 
        //This is the parametar that will be used in you lambda function 
        var param = Expression.Parameter(typeof(T)); 

        foreach (var group in groups) 
        { 
         // I start from a null expression so you don't use the silly 1 = 1 if this is a requirement for some reason you can make the 1 = 1 expression instead of null 
         Expression groupExp = null; 

         foreach (var condition in group) 
         { 
          Expression con; 
          //Just a simple type selector and remember switch is evil so you can do it another way 
          switch (condition.Type) 
          { 
//this creates the between NOTE if data types are not the same this can throw exceptions 
           case ConditionType.Range: 
            con = Expression.AndAlso(
             Expression.GreaterThanOrEqual(Expression.Property(param, condition.ColumnName), Expression.Constant(condition.Values[0])), 
             Expression.LessThanOrEqual(Expression.Property(param, condition.ColumnName), Expression.Constant(condition.Values[1]))); 
            break; 
           default: 
            con = Expression.Constant(true); 
            break; 
          } 
          // Builds an or if you need one so you dont use the 1 = 1 
          groupExp = groupExp == null ? con : Expression.OrElse(groupExp, con); 
         } 

         exp = exp == null ? groupExp : Expression.AndAlso(groupExp, exp); 
        } 

        return Expression.Lambda<Func<T, bool>>(exp,param); 
       } 
      } 

      static void Main(string[] args) 
      { 
       //Simple test data as an IQueriable same as EF or any ORM that supports linq. 
       var sales = new[] 
       { 
        new Sale{ A = 1, B = 2 , C = 1 }, 
        new Sale{ A = 4, B = 2 , C = 1 }, 
        new Sale{ A = 8, B = 4 , C = 1 }, 
        new Sale{ A = 16, B = 4 , C = 1 }, 
        new Sale{ A = 32, B = 2 , C = 1 }, 
        new Sale{ A = 64, B = 2 , C = 1 }, 
       }.AsQueryable(); 

       var conditions = new[] 
       { 
        new Condition { ColumnName = "A", Type = Condition.ConditionType.Range, Values= new object[]{ 0, 2 } }, 
        new Condition { ColumnName = "A", Type = Condition.ConditionType.Range, Values= new object[]{ 5, 60 } }, 
        new Condition { ColumnName = "B", Type = Condition.ConditionType.Range, Values= new object[]{ 1, 3 } }, 
        new Condition { ColumnName = "C", Type = Condition.ConditionType.Range, Values= new object[]{ 0, 3 } }, 
       }; 

       var exp = Condition.CreateExpression<Sale>(conditions); 
       //Under no circumstances compile the expression if you do you start using the IEnumerable and they are not converted to SQL but done in memory 
       var items = sales.Where(exp).ToArray(); 

       foreach (var sale in items) 
       { 
        Console.WriteLine($"new Sale{{ A = {sale.A}, B = {sale.B} , C = {sale.C} }}"); 
       } 

       Console.ReadLine(); 
      } 
+0

这很好。感兴趣的是你包括一个条件类型的范围。我有完全相同的东西,但省略了它以缩短例子。 – Valuator

+0

@Valuator我认为这就是为什么我加了它。这就是为什么我虽然你的SQL没有工作,但它缺少一个(。我建立了这么多次,这是显而易见的。 –