Search code examples
c#.netlinqexpression-trees

How to use multiple 'Where' expressions and chain them together with AND and OR using C#/.NET?


I am trying to make a filtering system in my web app. The problem is I don't know how many filters will be requested from my client to API. I've build it so the array of the filters comes from a single string like this: ?sizeFilters=big,small,medium

Then I use a string[] names = sizeFilters.Split(','); to get single expression like Where(x => x.listOfSizes.contains(names[index]));

I need also to make the chain of the expression using AND and OR because I am gonna use another filter for example: '?typeFilters=normal,extra,spicy'

So I need to make it that the whole expressions looks like this but it might be few times longer, it needs to work with different size of arrays:

return items 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")

Solution

  • The simplest option would be, as others have noted, to build your ORs using Enumerable.Contains within an expression; and to build your ANDs by calling Where multiple times.

    // 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));
    }
    

    If you want, you could wrap this logic into an extension method that takes an expression to filter against, and an IEnumerable<string> for the filter values; and constructs and applies the Contains method:

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

    and can be called like this:

    qry = qry
        .WhereValues(x => x.Size, sizeTerms)
        .WhereValues(x => x.Type, typeTerms);
    

    One caveat: the query will be built based on the values passed into the method; if they are later changed, the query won't reflect those changes. If this is an issue:

    • get the appropriate overload of Enumerable.Contains, instead of List.Contains, and
    • use the overload of Expression.Call which produces a static method call, instead of an instance method call.