How to simplify repetitive OR condition in Where(e => e.prop1.contains() || e.prop2.contains() || ...)

Viewed 219

I have a lot of code like this:

IQueryable<MyEntity> query...
query.Where(e => EF.Functions.Like(e.Property1,  pattern)
                || EF.Functions.Like(e.Property2, pattern)
                || EF.Functions.Like(e.Property3, pattern)
                ...);

How can I simplify it to remove the repetitive code to something like this:

query.Where(Search(pattern, e => e.Property1, e => e.Property2, e => e.Property3));  

or

query.Where(e => Search(pattern, e.Property1,  e.Property2,  e.Property3))  

EDIT: The query must be translated to SQL by EF, so the where condition can't be just any function that return boolean. It must be translatable Expression<Func<TEntity, bool>>

3 Answers

For top level IQueryable<T> (i.e. something which is not part of a query expression tree), this could be achieved with a custom extension method receiving value to match, predicate expression and list of expressions, and building or predicate from them. Something like this:

public static partial class QueryableExtensions
{
    public static IQueryable<T> WhereAnyMatch<T, V>(this IQueryable<T> source,
        V value, Expression<Func<V, V, bool>> match,
        params Expression<Func<T, V>>[] itemsToMatch)
    {
        var parameter = Expression.Parameter(typeof(T), "e");
        Expression<Func<V>> arg = () => value; // to let EF Core bind a parameter rather tnan a constant
        var body = itemsToMatch
            .Select(item => Expression.Invoke(match, Expression.Invoke(item, parameter), arg.Body))
            .Aggregate<Expression>(Expression.OrElse);
        var predicate = Expression.Lambda<Func<T, bool>>(body, parameter);
        return source.Where(predicate);
    }
}

and sample usage with your case would be:

.WhereAnyMatch(pattern, (item, arg) => EF.Functions.Like(item, arg),
    e => e.Property1, e => e.Property2, e => e.Property3);

Of course once you have that method, you could add other with commonly used match predicates, for instance Like:

public static partial class QueryableExtensions
{
    public static IQueryable<T> WhereAnyLike<T>(this IQueryable<T> source,
        string pattern, params Expression<Func<T, string>>[] itemsToMatch)
        => source.WhereAnyMatch(pattern, (item, arg) => EF.Functions.Like(item, arg), itemsToMatch);
}

and the sample code will be simplified as

.WhereAnyLike(pattern, e => e.Property1, e => e.Property2, e => e.Property3);

You can rewrite it to combine all the properties into an array and check that for a match:

query.Where(e =>
    new[]{ e.Property1, e.Property2, e.Property3}
      .Any(p => EF.Functions.Like(p, pattern) )
);

The equivalent SQL is:

WHERE EXISTS (SELECT 1
    FROM (VALUES
      (e.Property1),
      (e.Property2),
      (e.Property3)
    ) v(Prop)
    WHERE v.Prop LIKE @pattern
)

The equivalent &&/AND semantic is slightly different:

query.Where(e =>
    new[]{ e.Property1, e.Property2, e.Property3}
      .All(p => EF.Functions.Like(p, pattern) )
);

The equivalent SQL is a double-negative:

WHERE NOT EXISTS (SELECT 1
    FROM (VALUES
      (e.Property1),
      (e.Property2),
      (e.Property3)
    ) v(Prop)
    WHERE v.Prop NOT LIKE @pattern
)

One of the methods to prevent having to retype similar code over and over again, was invented in the late '60s. They called it a procedure.

So why not extend IQueryable with a method WhereLike? Once you've done that, you can intertwine it with other LINQ statements, as if it was a proper LINQ method.

If you are not familiar with extension methods, read extension methods demystified

We can create an extension method for two or three Like statemens, or maybe for any finite number.

public static IQueryable<T> WhereLike<T>(this IQueryable<T> source,
    Func<T, string> propertySelector1,
    Func<T, string> propertySelector2,
    string pattern)
{
    return source.Where(item => EF.Functions.Like(propertySelector1(item, pattern)
                             || EF.Functions.Like(propertySelector2(item, pattern));
}

(similar for 3 property selectors)

Usage:

Get Customers who's first or last name are like "John%"

var result = dbContext.Customers.
    .WhereLike(customer => customer.FirstName,
               customer => customer.LastName,
               "John%");
Related