【发布时间】:2021-02-12 08:56:44
【问题描述】:
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]
这样做的结果是,任何具有 null 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,网站 = dtoCustomer.Website})
编辑 2021/02/19
测绘计划:
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; }
}
ViewModel -
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>();
编辑 2021/02/20 - 手动排除状态
正如@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()
突然之间,Status 和 Customer 之间的理解关系丢失了。
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
编辑 2021/02/20 - 最新版本
似乎行为在最新版本中是相同的。
仅供参考:我们有一个场景是定期从另一个应用程序导入记录。我们希望使用内部连接来排除在另一个表中没有匹配记录的记录。然后这些记录将在稍后由导入过程更新。
但从应用程序的角度来看,它应该始终忽略这些记录,因此内部连接和状态是强制性的。但是我们将不得不使用 where 手动排除它们(根据 atiyar 的解决方案),以防止分页返回溢出的页数。
编辑 2021/02/20 - 进一步挖掘 这似乎是 EF 团队的设计选择和优化。这里的假设是,如果关系是非空的。然后加入不会被包括在性能提升中。解决这个问题的方法是@atiyar 所建议的。感谢大家@atiyar 和@Lucian-Bargaoanu 的帮助。
【问题讨论】:
-
好问题,我不包括 5 版本。 Microsoft.EntityFrameworkCore:3.1.12,Microsoft.EntityFrameworkCore.SqlServer:3.1.12,Microsoft.EntityFrameworkCore.Tools:3.1.12
-
由于技术要求,我无法迁移到 5.0。我将尝试将自动映射器回滚几个版本。
-
我不认为 AM 有问题 :) 在 5.0 上进行测试至少会告诉您问题是否已修复。
-
@Rtype 因此,如果您不使用
ProjectTo<>并使用带有.Select()的手动投影,您的第二个查询会产生预期的结果?如果是这样的话,你能分享那个手动投影查询吗? -
问题是你让 EF 认为
Status是必需的,这意味着[c].StatusId不能为空并且内部连接是合理的。因此,要么使StatusId不为空,要么添加一个谓词来获取具有状态的Custormers。永远不要将连接用作隐蔽谓词。
标签: sql-server automapper asp.net-core-3.1 entity-framework-core-3.1