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