У меня странная проблема с AutoMapper
(я использую .NET core 3.1 и AutoMapper 10.1.1)
Я делаю простой проект для списка и простой прогнозируемый счет для общих записей:
var data = Db.Customers
.Skip((1 - 1) * 25)
.Take(25)
.ProjectTo<CustomerViewModel>(Mapper.ConfigurationProvider)
.ToList();
var count = Db.Customers
.ProjectTo<CustomerViewModel>(Mapper.ConfigurationProvider)
.Count();
Первая строка создает ожидаемый SQL:
exec sp_executesql N'SELECT [c].[Code], [c].[Id], [c].[Name], [c].[Website], [s].Name
FROM [Customers] AS [c]
INNER JOIN [Status] AS [s] ON [s].id = [c].StatusId
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY',N'@__p_0 int,@__p_1 int',@__p_0=0,@__p_1=25
Вторая строка, Count(). Кажется, полностью игнорирует проекцию:
SELECT COUNT(*)
FROM [Customers] AS [c]
В результате любой клиент с нулевым StatusId
будет исключен из первого запроса, но включен в подсчет во втором. Что ломает пейджинг.
Я бы подумал, что проект должен создать что-то вроде:
SELECT COUNT(*)
FROM [Customers] AS [c]
INNER JOIN [Status] AS [s] ON [s].id = [c].StatusId
Кто-нибудь знает, почему Count() игнорирует ProjectTo<>
?
Изменить
План выполнения:
value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Domain.Customer]).Select(dtoCustomer => new CustomerViewModel() { Code = dtoCustomer.Code, Id = dtoCustomer.Id, Name = dtoCustomer.Name, StatusName = dtoCustomer.Status.Name, Website = dtoCustomer.Website})
Изменить план сопоставлений на 19 февраля 2021 г.
:
Сущности EF -
public class Customer
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public string Code { get; private set; }
public string Website { get; private set; }
public CustomerStatus Status { get; private set; }
public Customer() { }
}
public class CustomerStatus
{
public Guid Id { get; private set; }
public string Name { get; private set; }
}
ВьюМодель -
public class CustomerViewModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public string Website { get; set; }
public string StatusName { get; set; }
}
Отображение -
CreateMap<Customer, CustomerViewModel>();
Редактировать 20.02.2021 — Статус исключения вручную
Как указано в ответе @atiyar, вы можете вручную исключить статус. Это пересекает меня как обходной путь. Мои рассуждения таковы:
Если вы выполните этот запрос, как самый корневой запрос:
Db.Customers.ProjectTo<CustomerViewModel>(_mapper.ConfigurationProvider)
Вы получаете:
exec sp_executesql N'SELECT TOP(@__p_0) [c].[Id], [c].[Name], [c0].[Name]
AS [StatusName]
FROM [Customers] AS [c]
INNER JOIN [CustomerStatus] AS [c0] ON [c].[StatusId] = [c0].[Id]',N'@__p_0
int',@__p_0=5
Это показывает, что automapper понимает и может видеть, что существует необходимая связь между Status и Customer. Но когда вы применяете механизм подсчета:
Db.Customers.ProjectTo<CustomerViewModel>(_mapper.ConfigurationProvider).Count()
Внезапно понятные отношения между Статусом и Клиентом теряются.
SELECT COUNT(*)
FROM [Customers] AS [c]
По моему опыту работы с Linq, каждый шаг запроса изменяет предыдущий шаг предсказуемым образом. Я ожидал, что счет будет основан на первой команде и будет включать счет как часть этого.
Интересно, если вы выполните это:
_context.Customers.ProjectTo<CustomerViewModel>(_mapper.ConfigurationProvider).Take(int.MaxValue).Count()
Automapper применяет отношения, и результат такой, как я и ожидал:
exec sp_executesql N'SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [c].[Id], [c].[Name], [c0].[Name] AS [Name0], [c0].[Id]
AS [Id0]
FROM [Customers] AS [c]
INNER JOIN [CustomerStatus] AS [c0] ON [c].[StatusId] = [c0].[Id]
) AS [t]',N'@__p_0 int',@__p_0=2147483647
Редактировать 20 февраля 2021 г. — последняя версия
Кажется, поведение такое же в последней версии.
К вашему сведению: у нас есть сценарий, в котором записи регулярно импортируются из другого приложения. Мы надеялись использовать внутреннее соединение, чтобы исключить записи, которые не имеют совпадающей записи в другой таблице. Затем эти записи будут обновлены позднее в процессе импорта.
Но с точки зрения приложения он всегда должен игнорировать эти записи, поэтому внутреннее соединение и статус являются обязательными. Но нам придется вручную исключить их (в соответствии с решением atiyar), используя параметр where, чтобы предотвратить возврат раздутых номеров страниц при подкачке.
Редактировать 20 февраля 2021 г. – дальнейшие исследования Похоже, это выбор дизайна командой EF и оптимизация. Здесь предполагается, что если отношение не равно нулю. Тогда соединение не будет включено в качестве повышения производительности. Обойти это так, как предложил @atiyar. Спасибо за помощь всем @atiyar и @Lucian-Bargaoanu.
ProjectTo
запросом LINQ и удалить AM из уравнения. - person Lucian Bargaoanu   schedule 14.02.2021ProjectTo<>
и используете ручную проекцию с.Select()
? Если это так, не могли бы вы поделиться этим запросом ручной проекции? - person atiyar   schedule 19.02.2021Status
требуется, а это означает, что[c].StatusId
не допускает значение NULL, а внутреннее соединение оправдано. Поэтому либо сделайтеStatusId
ненулевым, либо добавьте предикат, чтобы получитьCustormers
, у которых есть статус. Соединения никогда не должны использоваться в качестве скрытых предикатов. - person Gert Arnold   schedule 19.02.2021