使用表达式树

时间:2017-09-07 22:09:33

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

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

ReferenceColumn(它们适用的Sale表中的列)

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

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

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

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数据库,所以我试图用Expression树重写它,我已经从未使用过。到目前为止,我已经得到了这个:

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。十进制不能在那里使用,它说它想要类型Sale,但我不知道该把它放在什么销售,我不确定我是否在这里正确的轨道。我也担心我的masterExpression每次都会重复,而不是像我对字符串生成器那样追加,但也许这样就可以了。

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

1 个答案:

答案 0 :(得分: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();
            }