我有一个系统,允许将与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每次都会重复,而不是像我对字符串生成器那样追加,但也许这样就可以了。
我正在寻找有关如何将此动态查询转换为表达式树的帮助,如果我不在这里,我会采用完全不同的方法。
答案 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();
}