如何使用多个 'Where' 表达式并使用 C#/.NET 将它们与 AND 和 OR 链接在一起?
How to use multiple 'Where' expressions and chain them together with AND and OR using C#/.NET?
我正在尝试在我的网络应用程序中制作一个过滤系统。问题是我不知道我的客户会向 API 请求多少过滤器。我已经构建了它,因此过滤器数组来自这样的单个字符串:?sizeFilters=big,small,medium
然后我使用 string[] names = sizeFilters.Split(',');
来获得像 Where(x => x.listOfSizes.contains(names[index]));
这样的单个表达式
我还需要使用 AND 和 OR 制作表达式链,因为我将使用另一个过滤器,例如:'?typeFilters=normal,extra,spicy'
所以我需要使整个表达式看起来像这样,但它可能会长几倍,它需要使用不同大小的数组:
return 项 Where size is big OR small OR medium AND Where type is normal OR extra OR spicy
Where(x => x.Sizes == "Small" || x => x.Sizes == "Medium" || x => x.Sizes == "Big" &&
x => x.Types == "normal" || x => x.Types == "extra" || x => x.Types == "Spicy")
我认为以下应该有效
var query = _context.Set<[Entity]>();
if (sizeFilterPresent)
{
query = query.Where(r => sizes.Contains(r.Size));
}
if(typesFilterPresent)
{
query = query.Where(r => types.Contains(r.Type));
}
var results = query.ToList();
你可以试试这个,
var result = data.Where(p => sizeFilters.Contains(p.Size) && typeFilters.Contains(p.Type));
您可以简单地多次调用 .Where
以将 AND 表达式组合在一起。动态 OR-ing 表达式在一起要困难得多。您需要重建表达式图以包含 OrElse
运算符,并确保所有表达式都基于相同的 ParameterExpression
.
public class Replacer : ExpressionVisitor
{
private readonly Dictionary<Expression, Expression> _replacements;
public Replacer(IEnumerable<Expression> before, IEnumerable<Expression> after)
{
_replacements = new Dictionary<Expression, Expression>(before.Zip(after, (a, b) => KeyValuePair.Create(a, b)));
}
public override Expression Visit(Expression node)
{
if (node != null && _replacements.TryGetValue(node, out var replace))
return base.Visit(replace);
return base.Visit(node);
}
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
if (expr1 == null)
return expr2;
if (expr2 == null)
return expr1;
return Expression.Lambda<Func<T, bool>>(
Expression.OrElse(
expr1.Body,
new Replacer(expr2.Parameters, expr1.Parameters).Visit(expr2.Body)
),
expr1.Parameters);
}
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
if (expr1 == null)
return expr2;
if (expr2 == null)
return expr1;
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(
expr1.Body,
new Replacer(expr2.Parameters, expr1.Parameters).Visit(expr2.Body)
),
expr1.Parameters);
}
// Usage
Expression<Func<TableObject, bool>> where = null;
if (...)
where = where.Or(x => sizeFilters.Contains(x.Size));
if (...)
where = where.Or(x => typeFilters.Contains(x.Type));
if (where!=null)
query = query.Where(where);
为了让它看起来整洁,我的建议是创建和扩展方法。这样您就可以像使用任何其他 LINQ 方法一样使用它。参见 extension methods demystified
假设您的来源是 IQuertyable<TSource>
。
public static IQueryable<TSource> WhereAnd<TSource>(
this IQueryable<TSource> source,
IEnumerable<Expression<Func<TSource,bool>>> filterPredicates)
{
// TODO: handle null source, expressions;
IQueryable<TSource> filteredSource = source;
foreach (var predicate in filterPredicates)
{
filteredSource = filteredSource.Where(predicate);
}
}
用法:
var predicates = new List<Expression<Func<TSource,bool>>>()
{
customer => customer.BirthDay.Year <= 1950,
customer => customer.CityId == GetCityId("New York"),
customer => customer.Gender == Gender.Male,
}
var oldNewYorkMaleCustomers = dbContext.Customers.WhereAnd(predicates).ToList();
注意:过滤谓词的空集合将不过滤谓词:您得到原始数据:
var emptyFilter = Queryable.Empty<Expression<Func<Customer, bool>>>();
var allCustomers = dbContext.Customers.WhereAnd(emptyFilter);
正如其他人指出的那样,最简单的选择是在表达式中使用 Enumerable.Contains
构建您的 OR
;并通过多次调用 Where
来构建您的 AND
。
// using these values as an example
string[] sizeTerms = /* initialize */;
string[] typeTerms = /* initialize */;
IQueryable<Item> items = /* initialize */
if (sizeTerms.Any()) {
items = items.Where(x => sizeTerms.Contains(x.Size));
}
if (typeTerms.Any()) {
items = items.Where(x => typeTerms.Contains(x.Type));
}
如果需要,您可以将此逻辑包装到一个扩展方法中,该方法采用一个表达式作为过滤依据,并为过滤值提供一个 IEnumerable<string>
;并构造并应用 Contains
方法:
// using System.Reflection
// using static System.Linq.Expressions.Expression
private static MethodInfo containsMethod = typeof(List<>).GetMethod("Contains");
public static IQueryable<TElement> WhereValues<TElement, TFilterTarget>(
this IQueryable<TElement> qry,
Expression<Func<TElement, TFilterTarget>> targetExpr,
IEnumerable<string> values
) {
var lst = values.ToList();
if (!lst.Any()) { return qry; }
return qry.Where(
Lambda<Expression<Func<TElement, bool>>>(
Call(
Constant(lst),
containsMethod.MakeGenericMethod(typeof(T)),
targetExpr.Body
),
targetExpr.Parameters.ToArray()
)
);
}
并且可以这样调用:
qry = qry
.WhereValues(x => x.Size, sizeTerms)
.WhereValues(x => x.Type, typeTerms);
一个警告:查询将基于传递给方法的值构建;如果稍后更改,查询将不会反映这些更改。如果这是一个问题:
- 获取
Enumerable.Contains
的适当重载,而不是 List.Contains
和
- 使用
Expression.Call
的重载产生静态方法调用,而不是实例方法调用。
我正在尝试在我的网络应用程序中制作一个过滤系统。问题是我不知道我的客户会向 API 请求多少过滤器。我已经构建了它,因此过滤器数组来自这样的单个字符串:?sizeFilters=big,small,medium
然后我使用 string[] names = sizeFilters.Split(',');
来获得像 Where(x => x.listOfSizes.contains(names[index]));
我还需要使用 AND 和 OR 制作表达式链,因为我将使用另一个过滤器,例如:'?typeFilters=normal,extra,spicy'
所以我需要使整个表达式看起来像这样,但它可能会长几倍,它需要使用不同大小的数组:
return 项 Where size is big OR small OR medium AND Where type is normal OR extra OR spicy
Where(x => x.Sizes == "Small" || x => x.Sizes == "Medium" || x => x.Sizes == "Big" &&
x => x.Types == "normal" || x => x.Types == "extra" || x => x.Types == "Spicy")
我认为以下应该有效
var query = _context.Set<[Entity]>();
if (sizeFilterPresent)
{
query = query.Where(r => sizes.Contains(r.Size));
}
if(typesFilterPresent)
{
query = query.Where(r => types.Contains(r.Type));
}
var results = query.ToList();
你可以试试这个,
var result = data.Where(p => sizeFilters.Contains(p.Size) && typeFilters.Contains(p.Type));
您可以简单地多次调用 .Where
以将 AND 表达式组合在一起。动态 OR-ing 表达式在一起要困难得多。您需要重建表达式图以包含 OrElse
运算符,并确保所有表达式都基于相同的 ParameterExpression
.
public class Replacer : ExpressionVisitor
{
private readonly Dictionary<Expression, Expression> _replacements;
public Replacer(IEnumerable<Expression> before, IEnumerable<Expression> after)
{
_replacements = new Dictionary<Expression, Expression>(before.Zip(after, (a, b) => KeyValuePair.Create(a, b)));
}
public override Expression Visit(Expression node)
{
if (node != null && _replacements.TryGetValue(node, out var replace))
return base.Visit(replace);
return base.Visit(node);
}
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
if (expr1 == null)
return expr2;
if (expr2 == null)
return expr1;
return Expression.Lambda<Func<T, bool>>(
Expression.OrElse(
expr1.Body,
new Replacer(expr2.Parameters, expr1.Parameters).Visit(expr2.Body)
),
expr1.Parameters);
}
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
{
if (expr1 == null)
return expr2;
if (expr2 == null)
return expr1;
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(
expr1.Body,
new Replacer(expr2.Parameters, expr1.Parameters).Visit(expr2.Body)
),
expr1.Parameters);
}
// Usage
Expression<Func<TableObject, bool>> where = null;
if (...)
where = where.Or(x => sizeFilters.Contains(x.Size));
if (...)
where = where.Or(x => typeFilters.Contains(x.Type));
if (where!=null)
query = query.Where(where);
为了让它看起来整洁,我的建议是创建和扩展方法。这样您就可以像使用任何其他 LINQ 方法一样使用它。参见 extension methods demystified
假设您的来源是 IQuertyable<TSource>
。
public static IQueryable<TSource> WhereAnd<TSource>(
this IQueryable<TSource> source,
IEnumerable<Expression<Func<TSource,bool>>> filterPredicates)
{
// TODO: handle null source, expressions;
IQueryable<TSource> filteredSource = source;
foreach (var predicate in filterPredicates)
{
filteredSource = filteredSource.Where(predicate);
}
}
用法:
var predicates = new List<Expression<Func<TSource,bool>>>()
{
customer => customer.BirthDay.Year <= 1950,
customer => customer.CityId == GetCityId("New York"),
customer => customer.Gender == Gender.Male,
}
var oldNewYorkMaleCustomers = dbContext.Customers.WhereAnd(predicates).ToList();
注意:过滤谓词的空集合将不过滤谓词:您得到原始数据:
var emptyFilter = Queryable.Empty<Expression<Func<Customer, bool>>>();
var allCustomers = dbContext.Customers.WhereAnd(emptyFilter);
正如其他人指出的那样,最简单的选择是在表达式中使用 Enumerable.Contains
构建您的 OR
;并通过多次调用 Where
来构建您的 AND
。
// using these values as an example
string[] sizeTerms = /* initialize */;
string[] typeTerms = /* initialize */;
IQueryable<Item> items = /* initialize */
if (sizeTerms.Any()) {
items = items.Where(x => sizeTerms.Contains(x.Size));
}
if (typeTerms.Any()) {
items = items.Where(x => typeTerms.Contains(x.Type));
}
如果需要,您可以将此逻辑包装到一个扩展方法中,该方法采用一个表达式作为过滤依据,并为过滤值提供一个 IEnumerable<string>
;并构造并应用 Contains
方法:
// using System.Reflection
// using static System.Linq.Expressions.Expression
private static MethodInfo containsMethod = typeof(List<>).GetMethod("Contains");
public static IQueryable<TElement> WhereValues<TElement, TFilterTarget>(
this IQueryable<TElement> qry,
Expression<Func<TElement, TFilterTarget>> targetExpr,
IEnumerable<string> values
) {
var lst = values.ToList();
if (!lst.Any()) { return qry; }
return qry.Where(
Lambda<Expression<Func<TElement, bool>>>(
Call(
Constant(lst),
containsMethod.MakeGenericMethod(typeof(T)),
targetExpr.Body
),
targetExpr.Parameters.ToArray()
)
);
}
并且可以这样调用:
qry = qry
.WhereValues(x => x.Size, sizeTerms)
.WhereValues(x => x.Type, typeTerms);
一个警告:查询将基于传递给方法的值构建;如果稍后更改,查询将不会反映这些更改。如果这是一个问题:
- 获取
Enumerable.Contains
的适当重载,而不是List.Contains
和 - 使用
Expression.Call
的重载产生静态方法调用,而不是实例方法调用。