[英]Building dynamic query in a loop using Expression trees
我有一個系統,可以將與Sales相關的不同條件存儲在數據庫中。 加載條件后,它們將用於構建查詢並返回所有適用的Sales。 條件對象如下所示:
ReferenceColumn(他們適用於Sale表中的列)
MinValue(參考列必須為最小值)
MaxValue(參考列必須為最大值)
使用上述條件的集合來搜索“銷售”。 相同類型的ReferenceColumns進行“或”運算,不同類型的ReferenceColumns進行“與”運算。 例如,如果我有三個條件:
ReferenceColumn:“價格”,最小值:“ 10”,最大值:“ 20”
ReferenceColumn:“價格”,最小值:“ 80”,最大值:“ 100”
ReferenceColumn:“年齡”,MinValue:“ 2”,MaxValue:“ 3”
查詢應返回價格在10-20或80-100之間的所有Sales,但前提是這些Sales的年齡在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,但我不知道在其中放置什么內容來進行銷售,而且我不確定這里的位置是否正確。 我還擔心我的masterExpression每次都在復制自己,而不是像對字符串生成器那樣進行追加,但這也許仍然可以工作。
我正在尋求有關如何將此動態查詢轉換為表達式樹的幫助,如果我不在這里,我願意采用一種完全不同的方法。
我想這對你有用
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();
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.