Automapper ProjectTo<> 不适用于 Count()
Automapper ProjectTo<> not working with Count()
我有一个关于 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})
编辑 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 - 最新版本
最新版本中的行为似乎相同。
仅供参考:我们有一个场景,其中定期从另一个应用程序导入记录。我们希望使用内部联接来排除在另一个 table 中没有匹配记录的记录。然后,这些记录将在稍后通过导入过程进行更新。
但是从应用程序的角度来看,它应该始终忽略那些记录,因此内部连接和状态是强制的。但是我们将不得不使用 where 手动排除它们(根据 atiyar 的解决方案),以防止分页返回过大的页数。
编辑 2021/02/20 - 进一步挖掘
这看起来确实是 EF 团队的设计选择和优化。这里的假设是如果关系是非空的。然后加入将不会作为性能提升包括在内。解决这个问题的方法正如@atiyar 所建议的那样。感谢大家@atiyar & @Lucian-Bargaoanu 的帮助。
我已经用 Entity Framework Core 3.1
和 AutoMapper 10.1.1
在 .NET Core 3.1
中测试了您的代码。并且-
您的第一个查询生成 LEFT JOIN
,而不是您发布的 INNER JOIN
。因此,该查询的结果不会排除任何 StatusId
为空的客户。并且,生成的 SQL 与 ProjectTo<>
和手动 EF 投影相同。我建议再次检查您的查询并生成 SQL 以确保。
您的第二个查询生成相同的 SQL,即您发布的 SQL,具有 ProjectTo<>
和手动 EF 投影。
一个适合你的解决方案:
如果我理解正确的话,你是想得到 -
- 列表
Customer
,在指定范围内,谁有相关的Status
- 您数据库中所有此类客户的数量。
尝试以下 -
- 在您的
Customer
模型中添加可为空的外键 属性 -
public Guid? StatusId { get; set; }
这将有助于简化您的查询及其生成的 SQL。
- 要获得预期列表,请将第一个查询修改为 -
var viewModels = Db.Customers
.Skip((1 - 1) * 25)
.Take(25)
.Where(p => p.StatusId != null)
.ProjectTo<CustomerViewModel>(_Mapper.ConfigurationProvider)
.ToList();
会生成如下SQL -
exec sp_executesql N'SELECT [t].[Code], [t].[Id], [t].[Name], [s].[Name] AS [StatusName], [t].[Website]
FROM (
SELECT [c].[Id], [c].[Code], [c].[Name], [c].[StatusId], [c].[Website]
FROM [Customers] AS [c]
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
) AS [t]
LEFT JOIN [Statuses] AS [s] ON [t].[StatusId] = [s].[Id]
WHERE [t].[StatusId] IS NOT NULL',N'@__p_0 int,@__p_1 int',@__p_0=0,@__p_1=25
- 要获得预期计数,请将第二个查询修改为 -
var count = Db.Customers
.Where(p => p.StatusId != null)
.Count();
会生成如下SQL -
SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[StatusId] IS NOT NULL
我有一个关于 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})
编辑 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 - 最新版本
最新版本中的行为似乎相同。
仅供参考:我们有一个场景,其中定期从另一个应用程序导入记录。我们希望使用内部联接来排除在另一个 table 中没有匹配记录的记录。然后,这些记录将在稍后通过导入过程进行更新。
但是从应用程序的角度来看,它应该始终忽略那些记录,因此内部连接和状态是强制的。但是我们将不得不使用 where 手动排除它们(根据 atiyar 的解决方案),以防止分页返回过大的页数。
编辑 2021/02/20 - 进一步挖掘 这看起来确实是 EF 团队的设计选择和优化。这里的假设是如果关系是非空的。然后加入将不会作为性能提升包括在内。解决这个问题的方法正如@atiyar 所建议的那样。感谢大家@atiyar & @Lucian-Bargaoanu 的帮助。
我已经用 Entity Framework Core 3.1
和 AutoMapper 10.1.1
在 .NET Core 3.1
中测试了您的代码。并且-
您的第一个查询生成
LEFT JOIN
,而不是您发布的INNER JOIN
。因此,该查询的结果不会排除任何StatusId
为空的客户。并且,生成的 SQL 与ProjectTo<>
和手动 EF 投影相同。我建议再次检查您的查询并生成 SQL 以确保。您的第二个查询生成相同的 SQL,即您发布的 SQL,具有
ProjectTo<>
和手动 EF 投影。
一个适合你的解决方案:
如果我理解正确的话,你是想得到 -
- 列表
Customer
,在指定范围内,谁有相关的Status
- 您数据库中所有此类客户的数量。
尝试以下 -
- 在您的
Customer
模型中添加可为空的外键 属性 -
public Guid? StatusId { get; set; }
这将有助于简化您的查询及其生成的 SQL。
- 要获得预期列表,请将第一个查询修改为 -
var viewModels = Db.Customers
.Skip((1 - 1) * 25)
.Take(25)
.Where(p => p.StatusId != null)
.ProjectTo<CustomerViewModel>(_Mapper.ConfigurationProvider)
.ToList();
会生成如下SQL -
exec sp_executesql N'SELECT [t].[Code], [t].[Id], [t].[Name], [s].[Name] AS [StatusName], [t].[Website]
FROM (
SELECT [c].[Id], [c].[Code], [c].[Name], [c].[StatusId], [c].[Website]
FROM [Customers] AS [c]
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
) AS [t]
LEFT JOIN [Statuses] AS [s] ON [t].[StatusId] = [s].[Id]
WHERE [t].[StatusId] IS NOT NULL',N'@__p_0 int,@__p_1 int',@__p_0=0,@__p_1=25
- 要获得预期计数,请将第二个查询修改为 -
var count = Db.Customers
.Where(p => p.StatusId != null)
.Count();
会生成如下SQL -
SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[StatusId] IS NOT NULL