如何使用多个 '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 的重载产生静态方法调用,而不是实例方法调用。