Automapper ProjectTo<> 不适用于 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]

这样做的结果是,任何具有 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; }
}

视图模型 -

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],[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],[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 的帮助。

解决方法

我已使用 .NET Core 3.1Entity Framework Core 3.1AutoMapper 10.1.1 中测试了您的代码。还有 -

  1. 您的第一个查询生成的是 LEFT JOIN,而不是您发布的 INNER JOIN。因此,该查询的结果不会排除任何 StatusId 为空的客户。并且,生成的 SQL 与 ProjectTo<> 和手动 EF 投影相同。我建议再次检查您的查询并生成 SQL 以确保。

  2. 您的第二个查询生成相同的 SQL,即您发布的 SQL,带有 ProjectTo<> 和手动 EF 投影。

适合您的解决方案:
如果我理解正确,您正在尝试获取 -

  1. 在指定范围内具有相关 CustomerStatus 列表
  2. 数据库中所有此类客户的数量。

尝试以下操作 -

  1. 在您的 Customer 模型中添加可为空的外键属性 -
public Guid? StatusId { get; set; }

这将有助于简化您的查询及其生成的 SQL。

  1. 要获得预期的列表,请将第一个查询修改为 -
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
  1. 要获得预期的计数,请将第二个查询修改为 -
var count = Db.Customers
            .Where(p => p.StatusId != null)
            .Count();

它会生成以下SQL -

SELECT COUNT(*)
FROM [Customers] AS [c]
WHERE [c].[StatusId] IS NOT NULL