#linq #linq-to-sql #entity-framework-core #automapper #ef-core-3.1
#linq #linq-to-sql #entity-framework-core #automapper #ef-core-3.1
Вопрос:
Прежде всего, полный сценарий можно найти здесь:https://dotnetfiddle.net/a2aIkJ
Я использую AutoMapper, чтобы включить фильтрацию (выражение LINQ) по атрибутам DTO и сопоставить эти выражения обратно с объектом базы данных. В конфигурации AutoMapper у меня есть следующий сценарий:
cfg.CreateMap<Car, CarDto>()
.ForMember(dest => dest.BrandFks, opt => opt.MapFrom(source => source.Wheels.Select(x => x.FkBrand)));
Когда я использую следующий код, преобразование LINQ в SQL работает должным образом:
// This code throws NO error
var working = context.Cars
.UseAsDataSource(mapper.ConfigurationProvider).For<CarDto>()
.Where(x => x.BrandFks.Any(y => y == 1))
.ToList();
Однако, когда я проверяю то же свойство на null:
// This code throws the error
var notWorking = context.Cars
.UseAsDataSource(mapper.ConfigurationProvider).For<CarDto>()
.Where(x => x.BrandFks == null)
.ToList();
возникает следующая ошибка:
Unhandled exception. System.InvalidOperationException: The LINQ expression 'DbSet<Car>
.Where(c => DbSet<Wheel>
.Where(w => EF.Property<Nullable<int>>(c, "Id") != null amp;amp; EF.Property<Nullable<int>>(c, "Id") == EF.Property<Nullable<int>>(w, "Id"))
.Select(w => w.FkBrand) == null)' 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 either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync().
Кто-нибудь знает, почему это не работает и как я мог бы решить эту проблему?
Редактировать — Уточнение: выражение фильтра определяется другим компонентом, который знает только о DTO и ничего о базе данных.
Комментарии:
1. Вместо проверки на
null
, не следует ли вам использовать!x.BrandFks.Any()
? Похоже, чтоBrandFks
это коллекция, которая не может иметь членов, но не может быть нулевой …?2. @NetMage Но проверка на
null
имеет смысл только при просмотре DTO, и в моем случае выражение фильтра устанавливается компонентом, который знает только о DTO и ничего о том, как оно сохраняется в базе данных3. Перевод показывает, что запрос все еще переводится в SQL, поэтому DTO не (точно) актуален. Вы могли бы поставить
.AsEnumerable()
передWhere
и извлечь все данные, связанные с клиентом, для тестирования?
Ответ №1:
Правильный подход
Вы должны использовать следующий запрос:
var correct = context.Cars
.UseAsDataSource(mapper.ConfigurationProvider).For<CarDto>()
.Where(x => !x.BrandFks.Any())
.ToList();
Если вы регистрируете сгенерированные запросы, становится очевидным, почему запрос необходимо переписать, как указано выше:
SELECT [c].[Id], [c].[Name], [w].[FkBrand], [w].[Id]
FROM [Cars] AS [c]
LEFT JOIN [Wheels] AS [w] ON [c].[Id] = [w].[Id]
WHERE NOT (EXISTS (
SELECT 1
FROM [Wheels] AS [w0]
WHERE [c].[Id] = [w0].[Id]))
ORDER BY [c].[Id], [w].[Id]
Automapper переводит ваше opt.MapFrom(source => source.Wheels.Select(x => x.FkBrand))
утверждение в LEFT JOIN
on Wheels
, а затем использует CarDto.BrandFks
как синоним (своего рода) для Wheels
таблицы.
Короче говоря, вы не можете проверить, является ли таблица NULL
, но вы можете проверить, пуста ли она.
Неоптимальная альтернатива
В качестве неоптимальной альтернативы вы могли бы просто переключиться на оценку клиента, но это приводит к потенциально большому набору результатов из базы данных, который затем фильтруется в памяти:
var suboptimal = context.Cars
.UseAsDataSource(mapper.ConfigurationProvider).For<CarDto>()
.AsEnumerable() // <-- switch to client evaluation
.Where(x => x.BrandFks == null)
.ToList();
Сгенерированный SQL выглядит следующим образом (он не содержит никакого фильтра, поскольку он фильтруется позже в памяти через LINQ to Objects):
SELECT [c].[Id], [c].[Name], [w].[FkBrand], [w].[Id]
FROM [Cars] AS [c]
LEFT JOIN [Wheels] AS [w] ON [c].[Id] = [w].[Id]
ORDER BY [c].[Id], [w].[Id]
Смотрите .NET Fiddle для получения кода в действии.
Комментарии:
1. Таким образом, это означало бы, что мне нужно перехватить
null
проверки в выражении DTO и изменить его на!Any()
? Возможно ли это?2. Если
source
илиWheels
вopt.MapFrom(source => source.Wheels.Select(x => x.FkBrand))
может бытьnull
, то вам обычно нужно проверить их там и защититься от этого. В противном случае вы этого не сделаете, потому что.Select(x => x.FkBrand)
всегда будет возвращать перечислимое значение (даже если оно пустое) и никогда не вернетсяnull
. Однако это предполагает, что выражение более или менее переведено AutoMapper как есть. Теоретически AutoMapper может просто переписать его совершенно другим способом (например, AutoMapper может вообще не нуждаться в каких-либоnull
проверках, потому что он все равно переводит выражение в SQL).3. Поведение по умолчанию в отношении нулевых коллекций описано в разделе Обработка нулевых коллекций .