Я использую EF Core 5.0.1 с веб-API ASP NET 5.0.1 и хочу построить запрос с помощью PredicateBuilder, используя LinqKit.Microsoft.EntityFrameworkCore 5.0.2.1
Для целей вопроса я упростил свою модель до:
public class User
{
public long IdUser { get; set; }
public string Name { get; set; }
public virtual ICollection<UserDepartment> UserDepartments { get; set; }
}
public class Department
{
public long IdDepartament { get; set; }
public string Name { get; set; }
public virtual ICollection<UserDepartment> UsersDepartment { get; set; }
}
public UserDepartment
{
public long IdUser { get; set; }
public long IdDepartment { get; set; }
public virtual Department { get; set; }
public virtual User { get; set; }
}
У одного User может быть много Departaments, а у одного Departament может быть много Users
Три модели имеют соответствующую таблицу в SQL Server и класс IEntityTypeConfiguration с установленными соответствующими отношениями.
Все, чего я хочу добиться, это найти любой User, принадлежащий любому Departament, который Department.Name находится в List<String>.
List<String> содержит список ключевых слов, а не точное название отдела.
Department таблица имеет такие строки:
А List<String> может быть любым ключевым словом, например «Админ», «Сал», «Отметить» и так далее.
Первая попытка
... должен был построить такой предикат:
List<string> kwDepartments = new List<String> {"mark","admin"};
var predicate = PredicateBuilder.New<User>(true);
predicate = predicate.And(x => x.UserDepartments.Where(y => kwDepartments.Any(c => y.Department.Name.Equals(c))).Any());
Это создает SQL с оператором IN, например:
...[t].[Name] IN (N'mark', N'admin'))
Очевидно, это не то, что я хочу, но если я использую .Contains вместо .Equals, возникает исключение
Выражение LINQ не может быть переведено
Я думаю, это потому, что я пытаюсь оценить непримитивное значение.
Вторая попытка
... должен был перебрать kwDepartments и добавить .Or для каждой строки, например:
foreach (string dep in kwDepartments )
{
predicateDep = predicateDep.Or(x => x.UserDepartments.Where(y=> y.Department.Name.Contains(dep)).Any());
}
predicate = predicate.And(predicateDep);
Это возвращает совпадение, которое я ожидаю, но также две проблемы.
Перевод SQL имеет низкую производительность, так как EF Core обрабатывает INNER JOIN для каждого ключевого слова. Неважно, состоит ли kwDepartment из двух или трех элементов, но со 100 или 1000 элементами будет недопустимо.
У каждого INNER JOIN на Departments таблице есть SELECT со всеми полями таблицы, и мне не нужно ни одно из них. Я пытался поместить оператор .Select(x=> x.Name) в предикат, чтобы он принимал только два поля, но это не дало никакого эффекта.
Третья попытка
... использовал полнотекстовый поиск с помощью EF.Functions.FreeText, но, похоже, это не имеет значения.
Моя цель - построить предикат, который переводит что-то похожее на:
SELECT [c.IdUser]. [c.Name]
FROM [User] AS [c]
WHERE EXISTS (
SELECT 1
FROM [UserDepartment] AS [u]
INNER JOIN (
SELECT [c0].[IdDepartment], [c0].[Name] <--ONLY NEED TWO FIELDS INSTEAD OF ALL FIELDS
FROM [Department] AS [c0]
) AS [t] ON [u].[IdDepartment] = [t].[IdDepartment]
WHERE ([c].[IdUser] = [u].[IdUser]) AND ([t].[Name] like (N'admin%') or [t].[Name] like (N'mark%')))
Использование оператора LIKE не обязательно, но я добавил его для лучшего понимания.
Еще раз спасибо!





Вы на правильном пути с предикатом Or, но вместо нескольких предикатов Or для user.UserDepatments.Any(single_match) вы должны создать один предикат на основе Or, который будет использоваться внутри одного user.UserDepatments.Any(multi_or_match).
Что-то вроде этого:
var departtmentPredicate = kwDepartments
.Select(kw => Linq.Expr((Department d) => EF.Functions.Like(d.Name, "%" + kw + "%")))
.Aggregate(PredicateBuilder.Or);
а потом
predicate = predicate.And(u => u.UserDepartments
.Select(ud => ud.Department) // navigate to department
.AsQueryable() // to be able to use departtmentPredicate expression directly
.Any(departtmentPredicate));
С этим кодом и списком примеров DbSet<User>().Where(predicate) переводится примерно так:
DECLARE @__p_1 nvarchar(4000) = N'%mark%';
DECLARE @__p_2 nvarchar(4000) = N'%admin%';
SELECT [u].[IdUser], [u].[Name]
FROM [User] AS [u]
WHERE EXISTS (
SELECT 1
FROM [UserDepartment] AS [u0]
INNER JOIN [Department] AS [d] ON [u0].[IdDepartment] = [d].[IdDepartament]
WHERE ([u].[IdUser] = [u0].[IdUser]) AND (([d].[Name] LIKE @__p_1) OR ([d].[Name] LIKE @__p_2)))
На самом деле вы можете видеть, что он объединяет две таблицы без специального выбора каких-либо полей, обратите внимание select 1 from ...
Моя ошибка, дополнительные поля извлекаются, потому что у меня есть дополнительный предикат запроса в классе IEntittypeConfiguration. Ваше решение было идеальным, как есть Еще раз спасибо, Иван!
Большое спасибо Иван. Ваше решение работает как шарм! :) Остается только одно, чтобы все было идеально. SQL, который создает ваше решение, включает SELECT со всеми полями (моя модель UserDepartment имеет 15 полей) во ВНУТРЕННЕМ СОЕДИНЕНИИ с UserDepartment, который мне не нужен, нужны только IdDepartment и Name. Есть ли способ взять только эти два поля из SQL?