Как создать предикат транслируемый в SQL?
При составлении LINQ, которые в дальнейшем транслируется в SQL, появилось большое количество повторяющихся условий, которые хочется вынести в одно место. Например:
// in PersonService
personsQuery = personsQuery.Where(x => x.Address.Name.Contains(searchString) || x.Address.Phone.Contains(searchString));
// in AddressService
addressQuery = addressQuery.Where(x => x.Name.Contains(searchString) || x.Phone.Contains(searchString));
// условия идентичны, разница только в сущности коллекции
В целом сущностей с такими условиями больше, но рассмотрим только Address и Person, чтобы показать пример, в котором Адрес рассматривается и как самостоятельная сущность и как часть другой
Если создать обычный предикат, то при кастинге LINQ-to-SQL получим ошибку:
// predicate
public bool FindAddressPredicate(Address address, string search)
{
return address.Name.Contains(searchString) || address.Phone.Contains(searchString);
}
// using
personsQuery = personsQuery.Where(x => FindAddressPredicate(x.Address, search));
The LINQ expression 'expression' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
Следующим шагом стала попытка написать метод расширения для коллекции. Проблемным моментом стал Expression для выбора Адреса из любого объекта.
// Extension method
public static IQueryable<T> ApplyAddressFilter<T>(this IQueryable<T> list, Expression<Func<T, Address>> addressSelector, string search)
{
return list.Where(x => addressSelector(x).Name.Contains(searchString) || addressSelector(x).Phone.Contains(searchString));
}
// using
personsQuery = personsQuery.ApplyAddressFilter(x => x.Address, search));
Такой код не компилируется по причине того, что addressSelector является Выражением, а не методом, в связи с чем возникает ошибка "Method name expected". Если же выражение скомпилировать, то получим ошибку трансляции
public static IQueryable<T> ApplyAddressFilter<T>(this IQueryable<T> list, Expression<Func<T, Address>> addressSelector, string search)
{
var selector = addressSelector.Compile();
return list.Where(x => selector(x).Name.Contains(searchString) || selector(x).Phone.Contains(searchString));
}
The LINQ expression '... Where(n => Invoke(__selector_0, n) ...' could not be translated
Можно ли создать Выражение которое позволяет указать, как выбрать необходимую для фильтрации сущность и при этом будет транслироваться в SQL?
Или это тупиковый способ и есть более подходящее решение?
UPD 1: Упрощенная структура классов:
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
}
Ответы (1 шт):
У меня получилось так:
public static IQueryable<T> ApplyAddressFilter<T>(this IQueryable<T> list, string search)
{
if (typeof(T) == typeof(Address))
return list.Where(x =>
(x as Address)!.Name.Contains(search) ||
(x as Address)!.Phone.Contains(search));
else
return list.Where(x =>
EF.Property<Address>(x, "Address").Name.Contains(search) ||
EF.Property<Address>(x, "Address").Phone.Contains(search));
}
Использование:
var addressQuery = db.Addresses.ApplyAddressFilter(searchString).ToList();
var personsQuery = db.Persons.ApplyAddressFilter(searchString).ToList();
Если запрос вызывается на таблице Address, то тип используется напрямую. На всех других таблицах будет использовано свойство Address через метод EF.Property.
Проверил на EF Core 6 и Sql Server. Генерируются нормальные sql-запросы.
Код в синтаксисе методов (method syntax) выглядит неуклюже из-за повторяющихся частей (x as Address) и EF.Property<Address>(x, "Address"). Если нужно будет проверять более двух свойств, то код станет выглядеть совсем громоздко.
Решить это можно с помощью оператора let в синтаксисе запросов (query syntax):
public static IQueryable<T> ApplyAddressFilter<T>(this IQueryable<T> list, string search)
{
if (typeof(T) == typeof(Address))
return from x in list
let address = x as Address
where
address.Name.Contains(search) ||
address.Phone.Contains(search)
select x;
else
return from x in list
let address = EF.Property<Address>(x, "Address")
where
address.Name.Contains(search) ||
address.Phone.Contains(search)
select x;
}