linq 如何使用多个“Where”表达式,并使用C#/.NET将它们与AND和OR链接在一起?

kh212irz  于 2023-07-31  发布在  C#
关注(0)|答案(5)|浏览(125)

我正在尝试在我的Web应用程序中创建一个过滤系统。问题是我不知道有多少过滤器将被请求从我的客户端到API。我已经构建了它,所以过滤器的数组来自一个像这样的字符串:第一个月
然后我使用string[] names = sizeFilters.Split(',');来获得类似Where(x => x.listOfSizes.contains(names[index]));的单个表达式
我还需要使用AND和OR来创建表达式的链,因为我将使用另一个过滤器,例如:'?typeFilters=normal,extra,spicy'
所以我需要使整个表达式看起来像这样,但它可能会长几倍,它需要使用不同大小的数组:
返回项目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")

字符串

2wnc66cl

2wnc66cl1#

您可以简单地多次调用.Where来一起执行AND表达式。对表达式进行动态OR运算要困难得多。您需要重新构建表达式图以包含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);

字符串
编辑;由于这是写的,EF核心现在暴露了ReplacingExpressionVisitor,可以用来代替上面的Replacer

mpbci0fu

mpbci0fu2#

正如其他人所指出的,最简单的选择是在表达式中使用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));
}

字符串
如果需要,可以将此逻辑 Package 到一个扩展方法中,该方法接受一个要过滤的表达式,并接受一个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的重载,它产生一个静态方法调用,而不是示例方法调用。
rdlzhqv9

rdlzhqv93#

我想下面应该可以

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();

字符串

ddrv8njm

ddrv8njm4#

你可以试试这个

var result = data.Where(p => sizeFilters.Contains(p.Size) && typeFilters.Contains(p.Type));

字符串

ctehm74n

ctehm74n5#

为了使它看起来整洁,我的建议是创建和扩展方法。这样,您就可以像使用任何其他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();


注意:filterpredicates的空集合将不通过任何 predicate 进行过滤:你会得到原始数据:

var emptyFilter = Queryable.Empty<Expression<Func<Customer, bool>>>();
var allCustomers = dbContext.Customers.WhereAnd(emptyFilter);

相关问题