【问题标题】:Building dynamic query in a loop using Expression trees使用表达式树在循环中构建动态查询
【发布时间】:2018-02-16 18:00:44
【问题描述】:

我有一个系统,允许将与销售有关的不同标准存储在数据库中。加载条件后,它们用于构建查询并返回所有适用的销售额。条件对象如下所示:

ReferenceColumn(他们适用的销售表中的列)

MinValue(参考列必须是最小值)

MaxValue(引用列必须是最大值)

使用上述条件的集合完成对销售的搜索。相同类型的 ReferenceColumns 被 OR'd 在一起,不同类型的 ReferenceColumns 被 AND'd 在一起。例如,如果我有三个标准:

ReferenceColumn:'Price',MinValue:'10',MaxValue:'20'

ReferenceColumn:'Price',MinValue:'80',MaxValue:'100'

ReferenceColumn:'Age',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();
}

事实上这在我们的数据库中运行良好,但它与其他集合不兼容,特别是我们用于 UnitTesting 的 InMemory 数据库,所以我尝试使用表达式树重写它,我已经以前从未使用过。到目前为止,我已经得到了这个:

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 不能在那里使用,它说它需要类型 Sale,但我不知道该为 Sales 放什么,而且我不确定我是否在正确的轨道上。我还担心我的 masterExpression 每次都在复制自己,而不是像我对字符串生成器所做的那样追加,但也许这无论如何都会起作用。

我正在寻求有关如何将此动态查询转换为表达式树的帮助,如果我不在此处,我愿意采用完全不同的方法。

【问题讨论】:

  • 您的原始代码有效吗?这不应该工作,你为什么使用 1 = 1 和 1 = 0?
  • 是的,如果集合是使用 SQL Server 的 DbContext 的一部分,它就可以工作。 1=1 和 1=0 在那里,所以我总是可以将 'AND'/'OR' 附加到查询字符串,而不必处理第一次迭代的特殊情况等。
  • 尝试使用 LINQKit (albahari.com/nutshell/linqkit.aspx),它会更容易。该页面说:使用 LINQKit,您可以: ...动态构建谓词
  • 我可能会再看看 LINQKit。我记得当我第一次处理这个问题时看过它,但选择使用 SQL 查询以避免引入另一个第三方库。
  • 好吧,如果您可以在数据库中使用 LINQ,那么使用 LINQKit 应该没问题。我多次使用它来构建动态查询(用于数据库和 SharePoint),如果没有它,我什至不会尝试 :)

标签: c# entity-framework linq expression-trees


【解决方案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();
            }

【讨论】:

  • 效果很好。有兴趣您包含范围的条件类型。我有完全相同的东西,但省略了它以使示例更短。
  • @Valuator 我想这就是我添加它的原因。这就是为什么我虽然你的 SQL 没有工作但缺少一个 ( 。我构建了很多次这样的东西,很明显。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-05-17
  • 1970-01-01
相关资源
最近更新 更多