如何在 .NET 中创建表达式生成器
How to create an Expression builder in .NET
我已经编写了一些代码来允许过滤我们网站上的产品,但我却闻到了一股非常难闻的代码味道。用户可以 select 1-* 这些过滤器,这意味着我需要使用 WHERE
子句来具体说明。
我想我正在寻找一种构建 lambda 表达式的方法,因此对于每个过滤器我都可以 'modify' 我的 WHERE
子句 - 但我不确定如何在 . NET,一定有办法。
当前状态的代码(有效硬编码,不是动态的,添加更多过滤器选项会很痛苦)。
public static class AgeGroups
{
public static Dictionary<string, int> Items = new Dictionary<string, int>(){
{ "Modern (Less than 10 years old)", 1 },
{ "Retro (10 - 20 years old)", 2 },
{ "Vintage(20 - 70 years old)", 3 },
{ "Antique(70+ years old)", 4 }
};
public static IQueryable<ProductDTO> FilterAgeByGroup(IQueryable<ProductDTO> query, List<string> filters)
{
var values = new List<int>();
var currentYear = DateTime.UtcNow.Year;
foreach (var key in filters)
{
var matchingValue = Items.TryGetValue(key, out int value);
if (matchingValue)
{
values.Add(value);
}
}
if (Utility.EqualsIgnoringOrder(values, new List<int> { 1 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 10);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2 }))
{
query = query.Where(x => x.YearManufactured <= currentYear - 10 && x.YearManufactured >= currentYear - 20);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 3 }))
{
query = query.Where(x => x.YearManufactured <= currentYear - 20 && x.YearManufactured >= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 4 }))
{
query = query.Where(x => x.YearManufactured <= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2}))
{
query = query.Where(x => x.YearManufactured >= currentYear - 20);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 3 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 10 || (x.YearManufactured <= currentYear - 20 && x.YearManufactured >= currentYear - 70));
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 4 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 10 || x.YearManufactured <= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2, 3 }))
{
query = query.Where(x => x.YearManufactured <= currentYear - 10 && x.YearManufactured >= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2, 4 }))
{
query = query.Where(x => (x.YearManufactured <= currentYear - 10 && x.YearManufactured >= currentYear - 20)
|| x.YearManufactured <= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2, 3 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2, 4 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 20 || x.YearManufactured <= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2, 3, 4}))
{
query = query.Where(x => x.YearManufactured <= currentYear - 10);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 3, 4}))
{
query = query.Where(x => x.YearManufactured >= currentYear - 10 || x.YearManufactured <= 20);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2, 3, 4 }))
{
// all
}
return query;
}
}
看来你遇到了组合爆炸。您可以使用修改后的 Items
集合静态声明简单案例:
static Dictionary<string, Expression<Func<int, int, bool>>> Items
= new Dictionary<string, Expression<Func<int, int, bool>>>
{
{
"Modern (Less than 10 years old)",
(yearManufactured, currentYear) => yearManufactured >= currentYear - 10
},
{
"Retro (10 - 20 years old)",
(yearManufactured, currentYear) => yearManufactured <= currentYear - 10 && yearManufactured >= currentYear - 20
},
{
"Vintage(20 - 70 years old)",
(yearManufactured, currentYear) => yearManufactured <= currentYear - 20 && yearManufactured >= currentYear - 70
},
{
"Antique(70+ years old)",
(yearManufactured, currentYear) => yearManufactured <= currentYear - 70
}
};
现在您可以使用 Linq 表达式动态组合简单案例 OrElse
。试试这个代码:
public static IQueryable<ProductDTO> FilterAgeByGroup(
IQueryable<ProductDTO> query, List<string> filters)
{
var conditions = new List<Expression>();
foreach (var key in filters)
if (Items.TryGetValue(key, out Expression<Func<int, int, bool>> value))
conditions.Add(value);
// return as is if there no conditions
if (!conditions.Any())
return query;
var x = Expression.Parameter(typeof(ProductDTO), "x");
var yearManufactured = Expression.PropertyOrField(x, "YearManufactured");
var currentYear = Expression.Constant(DateTime.UtcNow.Year);
var body = conditions.Aggregate(
(Expression) Expression.Constant(false), // ignore item by default
(c, n) => Expression.OrElse(c, Expression.Invoke(n, yearManufactured, currentYear)));
var lambda = Expression.Lambda<Func<ProductDTO, bool>>(body, x);
return query.Where(lambda);
}
我最近 运行 亲自解决了这个问题。通过另一个关于 SO 的问题的帮助,我找到了 http://www.albahari.com/nutshell/predicatebuilder.aspx。基本上,您想构建一个谓词并将其传递到查询的 where 子句中。
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> where1,
Expression<Func<T, bool>> where2)
{
InvocationExpression invocationExpression = Expression.Invoke(where2,
where1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(where1.Body,
invocationExpression), where1.Parameters);
}
public static IQueryable<ProductDTO> FilterAgeByGroup(IQueryable<ProductDTO> query,
List<string> filters, int currentYear)
{
var values = new HashSet<int>();
//Default value
Expression<Func<ProductDTO, bool>> predicate = (ProductDTO) => false;
foreach (var key in filters)
{
var matchingValue = Items.TryGetValue(key, out int value);
if (matchingValue)
{
values.Add(value);
}
}
if (values.Count == 0)
return query;
if (values.Contains(1))
{
predicate = predicate.Or(x => x.YearManufactured >= currentYear - 10);
}
if (values.Contains(2))
{
predicate = predicate.Or(x => x.YearManufactured <= currentYear - 10 &&
x.YearManufactured >= currentYear - 20);
}
if (values.Contains(3))
{
predicate = predicate.Or(x => x.YearManufactured <= currentYear - 20 &&
x.YearManufactured >= currentYear - 70);
}
if (values.Contains(4))
{
predicate = predicate.Or(x => x.YearManufactured <= currentYear - 70);
}
return query.Where(predicate);
}
使用LINQKit,您可以轻松组合谓词。此外,没有理由将 filters
List
翻译成另一个 List
只是为了处理它们,一旦你可以组合它们,你只需添加传入的每个过滤器。
public static class AgeGroups {
public static Dictionary<string, int> Items = new Dictionary<string, int>(){
{ "Modern (Less than 10 years old)", 1 },
{ "Retro (10 - 20 years old)", 2 },
{ "Vintage(20 - 70 years old)", 3 },
{ "Antique(70+ years old)", 4 }
};
public static IQueryable<ProductDTO> FilterAgeByGroup(IQueryable<ProductDTO> query, List<string> filters) {
var currentYear = DateTime.UtcNow.Year;
var pred = PredicateBuilder.New<ProductDTO>();
foreach (var fs in filters) {
if (Items.TryGetValue(fs, out var fv)) {
switch (fv) {
case 1:
pred = pred.Or(p => currentYear-p.YearManufactured < 10);
break;
case 2:
pred = pred.Or(p => 10 <= currentYear-p.YearManufactured && currentYear-p.YearManufactured <= 20);
break;
case 3:
pred = pred.Or(p => 20 <= currentYear-p.YearManufactured && currentYear-p.YearManufactured <= 70);
break;
case 4:
pred = pred.Or(p => 70 <= currentYear-p.YearManufactured);
break;
}
}
}
return query.Where(pred);
}
}
我建议动态构造要传递给 Where
的表达式(如 ;但不使用任何编译器生成的表达式,只使用 System.Linq.Expressions.Expression
.
首先用每个标准的最小和最大年龄的值元组定义你的原始字典:
// using static System.Linq.Expressions.Expression
public static Dictionary<string, (int code, int? min, int? max)> Items = new Dictionary<string, (int code, int? min, int? max)>(){
{ "Modern (Less than 10 years old)", (1, null, 10) },
{ "Retro (10 - 20 years old)", (2, 10, 20) },
{ "Vintage(20 - 70 years old)", (3, 20, 70) },
{ "Antique(70+ years old)", (4, 70, null) }
};
然后您可以动态构建谓词,根据传入的过滤器及其匹配条件添加条件:
public static IQueryable<ProductDTO> FilterAgeByGroup(
IQueryable<ProductDTO> query,
List<string> filters)
{
var criteria = filters
.Select(filter => {
Items.TryGetValue(filter, out var criterion);
return criterion; // if filter is not in Items.Keys, code will be 0
})
.Where(criterion => criterion.code > 0) // excludes filters not matched in Items
.ToList();
if (!criteria.Any()) { return query; }
var type = typeof(ProductDTO);
var x = Parameter(t);
// creates an expression that represents the number of years old this ProductDTO is:
// 2019 - x.YearManufactured
var yearsOldExpr = Subtract(
Constant(DateTime.UtcNow.Year),
Property(x, t.GetProperty("YearManufactured"))
);
var filterExpressions = new List<Expression>();
foreach (var criterion in criteria) {
Expression minExpr = null;
if (criterion.min != null) {
// has to be at least criteria.min years old; eqivalent of:
// 2019 - x.YearManufactured >= 10
minExpr = GreaterThanOrEqual(
yearsOldExpr,
Constant(criterion.min)
);
}
Expression maxExpr = null;
if (criterion.max != null) {
// has to be at least criteria.max years old; equivalent of
// 2019 - x.YearManufactured <= 20
maxExpr = LessThanOrEqual(
yearsOldExpr,
Constant(criterion.max)
)
}
if (minExpr != null && maxExpr != null) {
filterExpressions.Add(AndAlso(minExpr, maxExpr));
} else {
filterExpressions.Add(minExpr ?? maxExpr); // They can't both be null; we've already excluded that possibility above
}
}
Expression body = filterExpressions(0);
foreach (var filterExpression in filterExpressions.Skip(1)) {
body = OrElse(body, filterExpression);
}
return query.Where(
Lambda<Func<ProductDTO, bool>>(body, x)
);
}
我已经编写了一些代码来允许过滤我们网站上的产品,但我却闻到了一股非常难闻的代码味道。用户可以 select 1-* 这些过滤器,这意味着我需要使用 WHERE
子句来具体说明。
我想我正在寻找一种构建 lambda 表达式的方法,因此对于每个过滤器我都可以 'modify' 我的 WHERE
子句 - 但我不确定如何在 . NET,一定有办法。
当前状态的代码(有效硬编码,不是动态的,添加更多过滤器选项会很痛苦)。
public static class AgeGroups
{
public static Dictionary<string, int> Items = new Dictionary<string, int>(){
{ "Modern (Less than 10 years old)", 1 },
{ "Retro (10 - 20 years old)", 2 },
{ "Vintage(20 - 70 years old)", 3 },
{ "Antique(70+ years old)", 4 }
};
public static IQueryable<ProductDTO> FilterAgeByGroup(IQueryable<ProductDTO> query, List<string> filters)
{
var values = new List<int>();
var currentYear = DateTime.UtcNow.Year;
foreach (var key in filters)
{
var matchingValue = Items.TryGetValue(key, out int value);
if (matchingValue)
{
values.Add(value);
}
}
if (Utility.EqualsIgnoringOrder(values, new List<int> { 1 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 10);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2 }))
{
query = query.Where(x => x.YearManufactured <= currentYear - 10 && x.YearManufactured >= currentYear - 20);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 3 }))
{
query = query.Where(x => x.YearManufactured <= currentYear - 20 && x.YearManufactured >= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 4 }))
{
query = query.Where(x => x.YearManufactured <= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2}))
{
query = query.Where(x => x.YearManufactured >= currentYear - 20);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 3 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 10 || (x.YearManufactured <= currentYear - 20 && x.YearManufactured >= currentYear - 70));
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 4 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 10 || x.YearManufactured <= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2, 3 }))
{
query = query.Where(x => x.YearManufactured <= currentYear - 10 && x.YearManufactured >= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2, 4 }))
{
query = query.Where(x => (x.YearManufactured <= currentYear - 10 && x.YearManufactured >= currentYear - 20)
|| x.YearManufactured <= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2, 3 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2, 4 }))
{
query = query.Where(x => x.YearManufactured >= currentYear - 20 || x.YearManufactured <= currentYear - 70);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 2, 3, 4}))
{
query = query.Where(x => x.YearManufactured <= currentYear - 10);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 3, 4}))
{
query = query.Where(x => x.YearManufactured >= currentYear - 10 || x.YearManufactured <= 20);
}
else if (Utility.EqualsIgnoringOrder(values, new List<int> { 1, 2, 3, 4 }))
{
// all
}
return query;
}
}
看来你遇到了组合爆炸。您可以使用修改后的 Items
集合静态声明简单案例:
static Dictionary<string, Expression<Func<int, int, bool>>> Items
= new Dictionary<string, Expression<Func<int, int, bool>>>
{
{
"Modern (Less than 10 years old)",
(yearManufactured, currentYear) => yearManufactured >= currentYear - 10
},
{
"Retro (10 - 20 years old)",
(yearManufactured, currentYear) => yearManufactured <= currentYear - 10 && yearManufactured >= currentYear - 20
},
{
"Vintage(20 - 70 years old)",
(yearManufactured, currentYear) => yearManufactured <= currentYear - 20 && yearManufactured >= currentYear - 70
},
{
"Antique(70+ years old)",
(yearManufactured, currentYear) => yearManufactured <= currentYear - 70
}
};
现在您可以使用 Linq 表达式动态组合简单案例 OrElse
。试试这个代码:
public static IQueryable<ProductDTO> FilterAgeByGroup(
IQueryable<ProductDTO> query, List<string> filters)
{
var conditions = new List<Expression>();
foreach (var key in filters)
if (Items.TryGetValue(key, out Expression<Func<int, int, bool>> value))
conditions.Add(value);
// return as is if there no conditions
if (!conditions.Any())
return query;
var x = Expression.Parameter(typeof(ProductDTO), "x");
var yearManufactured = Expression.PropertyOrField(x, "YearManufactured");
var currentYear = Expression.Constant(DateTime.UtcNow.Year);
var body = conditions.Aggregate(
(Expression) Expression.Constant(false), // ignore item by default
(c, n) => Expression.OrElse(c, Expression.Invoke(n, yearManufactured, currentYear)));
var lambda = Expression.Lambda<Func<ProductDTO, bool>>(body, x);
return query.Where(lambda);
}
我最近 运行 亲自解决了这个问题。通过另一个关于 SO 的问题的帮助,我找到了 http://www.albahari.com/nutshell/predicatebuilder.aspx。基本上,您想构建一个谓词并将其传递到查询的 where 子句中。
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> where1,
Expression<Func<T, bool>> where2)
{
InvocationExpression invocationExpression = Expression.Invoke(where2,
where1.Parameters.Cast<Expression>());
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(where1.Body,
invocationExpression), where1.Parameters);
}
public static IQueryable<ProductDTO> FilterAgeByGroup(IQueryable<ProductDTO> query,
List<string> filters, int currentYear)
{
var values = new HashSet<int>();
//Default value
Expression<Func<ProductDTO, bool>> predicate = (ProductDTO) => false;
foreach (var key in filters)
{
var matchingValue = Items.TryGetValue(key, out int value);
if (matchingValue)
{
values.Add(value);
}
}
if (values.Count == 0)
return query;
if (values.Contains(1))
{
predicate = predicate.Or(x => x.YearManufactured >= currentYear - 10);
}
if (values.Contains(2))
{
predicate = predicate.Or(x => x.YearManufactured <= currentYear - 10 &&
x.YearManufactured >= currentYear - 20);
}
if (values.Contains(3))
{
predicate = predicate.Or(x => x.YearManufactured <= currentYear - 20 &&
x.YearManufactured >= currentYear - 70);
}
if (values.Contains(4))
{
predicate = predicate.Or(x => x.YearManufactured <= currentYear - 70);
}
return query.Where(predicate);
}
使用LINQKit,您可以轻松组合谓词。此外,没有理由将 filters
List
翻译成另一个 List
只是为了处理它们,一旦你可以组合它们,你只需添加传入的每个过滤器。
public static class AgeGroups {
public static Dictionary<string, int> Items = new Dictionary<string, int>(){
{ "Modern (Less than 10 years old)", 1 },
{ "Retro (10 - 20 years old)", 2 },
{ "Vintage(20 - 70 years old)", 3 },
{ "Antique(70+ years old)", 4 }
};
public static IQueryable<ProductDTO> FilterAgeByGroup(IQueryable<ProductDTO> query, List<string> filters) {
var currentYear = DateTime.UtcNow.Year;
var pred = PredicateBuilder.New<ProductDTO>();
foreach (var fs in filters) {
if (Items.TryGetValue(fs, out var fv)) {
switch (fv) {
case 1:
pred = pred.Or(p => currentYear-p.YearManufactured < 10);
break;
case 2:
pred = pred.Or(p => 10 <= currentYear-p.YearManufactured && currentYear-p.YearManufactured <= 20);
break;
case 3:
pred = pred.Or(p => 20 <= currentYear-p.YearManufactured && currentYear-p.YearManufactured <= 70);
break;
case 4:
pred = pred.Or(p => 70 <= currentYear-p.YearManufactured);
break;
}
}
}
return query.Where(pred);
}
}
我建议动态构造要传递给 Where
的表达式(如 System.Linq.Expressions.Expression
.
首先用每个标准的最小和最大年龄的值元组定义你的原始字典:
// using static System.Linq.Expressions.Expression
public static Dictionary<string, (int code, int? min, int? max)> Items = new Dictionary<string, (int code, int? min, int? max)>(){
{ "Modern (Less than 10 years old)", (1, null, 10) },
{ "Retro (10 - 20 years old)", (2, 10, 20) },
{ "Vintage(20 - 70 years old)", (3, 20, 70) },
{ "Antique(70+ years old)", (4, 70, null) }
};
然后您可以动态构建谓词,根据传入的过滤器及其匹配条件添加条件:
public static IQueryable<ProductDTO> FilterAgeByGroup(
IQueryable<ProductDTO> query,
List<string> filters)
{
var criteria = filters
.Select(filter => {
Items.TryGetValue(filter, out var criterion);
return criterion; // if filter is not in Items.Keys, code will be 0
})
.Where(criterion => criterion.code > 0) // excludes filters not matched in Items
.ToList();
if (!criteria.Any()) { return query; }
var type = typeof(ProductDTO);
var x = Parameter(t);
// creates an expression that represents the number of years old this ProductDTO is:
// 2019 - x.YearManufactured
var yearsOldExpr = Subtract(
Constant(DateTime.UtcNow.Year),
Property(x, t.GetProperty("YearManufactured"))
);
var filterExpressions = new List<Expression>();
foreach (var criterion in criteria) {
Expression minExpr = null;
if (criterion.min != null) {
// has to be at least criteria.min years old; eqivalent of:
// 2019 - x.YearManufactured >= 10
minExpr = GreaterThanOrEqual(
yearsOldExpr,
Constant(criterion.min)
);
}
Expression maxExpr = null;
if (criterion.max != null) {
// has to be at least criteria.max years old; equivalent of
// 2019 - x.YearManufactured <= 20
maxExpr = LessThanOrEqual(
yearsOldExpr,
Constant(criterion.max)
)
}
if (minExpr != null && maxExpr != null) {
filterExpressions.Add(AndAlso(minExpr, maxExpr));
} else {
filterExpressions.Add(minExpr ?? maxExpr); // They can't both be null; we've already excluded that possibility above
}
}
Expression body = filterExpressions(0);
foreach (var filterExpression in filterExpressions.Skip(1)) {
body = OrElse(body, filterExpression);
}
return query.Where(
Lambda<Func<ProductDTO, bool>>(body, x)
);
}