如何在 .NET 开发中将 AutoMapper 与 IQueryable 配合使用

作者:微信公众号:【架构师老卢】
5-2 9:49
100

在快节奏的 .NET 开发世界中,高效的数据映射至关重要,AutoMapper 库成为救星。它还通过其扩展方法与 EF Core 兼容。这两者的结合可以增强开发流程,从而实现受控的简化数据映射过程。下面我将深入探讨探索这个充满活力的二人组的细节。

了解 AutoMapper 🧩

在开始之前,让我们花点时间了解一下 AutoMapper 是什么。AutoMapper 是一个对象到对象库,它使程序员能够根据某些规范和约定将 A 类型的特定对象映射到 B 类型的另一个对象。因此,AutoMapper 不会像下面所示,自己编写样板代码并手动进行映射,而是为我们处理它。

手动映射 📋

var customer = //... Loading the customer from the database  
var customerDto = new CustomerDto();  
customerDto.FirstName = customer.FirstName;  
customerDto.LastName = customer.LastName;  
customerDto.Phone = customer.Phone;  
customerDto.Email = customer.Email;  
customerDto.NumberOfOrders = customer.Orders.Count;

使用自动测绘器🤖进行制图

var customer = //... Loading the customer from the database  
var customerDto = _mapper.Map<Customer, CustomerDto>(customer);

通知 AutoMapper 我的自定义映射(例如,.NumberOfOrders = 客户。Orders.Count),我已将以下配置添加到我的 MappingProfiles 类中

public class MappingProfiles : Profile  
{  
    public MappingProfiles()  
    {  
        CreateMap<Customer, CustomerDto>()  
            .ForMember(dest => dest.NumberOfOrders,   
                    opt => opt.MapFrom(src => src.Orders.Count));  
    }  
}

我们可以观察到,我们的代码行从 7 行减少到 2 行。在给定的琐碎场景中,它可能看起来并不重要。但是,在某些情况下,我们的实体将拥有数十个属性,从而导致更多的样板映射代码。因此,代码的可读性会降低,因此可维护性降低。此外,需要注意的是,手动映射属性容易出错,毕竟我们是人类,我们可能会忘记映射特定属性,从而导致意外行为。

AutoMapper 在自动执行对象之间的映射方面的作用可以节省开发人员的时间和精力。

英孚🗄️的强大功能

如果你曾经构建过 .NET Core Web API,则必须熟悉实体框架。实体框架 (EF) 是一个对象关系映射器 (ORM),它允许我们将对象模型(也称为实体)转换为表。基本上,它使我们能够仅使用类创建应用程序的整个数据库模式。此外,EF 还提供了一个抽象层,用于执行各种数据库查询,例如从表中选择数据、更新和管理数据。所有这些都不需要编写一个单一的 SQL 语句。

在软件开发的上下文中,EF 可以通过处理我们的大部分数据库 DML 和 DDL 操作来显着加快应用程序的开发过程。

让我们假设以下场景:

我们的任务是创建一个简单的客户订单管理系统。下面我们通过扩展 EF 的 DbContext 基类来配置数据库。

public class ApplicationDbContext : DbContext  
{  
    public DbSet<Customer> Customers { get; set; }  
    public DbSet<Order> Orders { get; set; }  
  
    // Contructors are omitted for brievity  
    // ..  
  
    protected override void OnModelCreating(ModelBuilder modelBuilder)  
    {  
        // Configure the one-to-many relationship between Customer and Order  
        modelBuilder.Entity<Customer>()  
            .HasMany(c => c.Orders)  
            .WithOne(o => o.Customer)  
            .HasForeignKey(o => o.CustomerId)  
            .OnDelete(DeleteBehavior.Cascade);  
    }  
}

使用以下实体:

public class Customer  
{  
    public int Id { get; set; }  
    public string FirstName { get; set; }  
    public string LastName { get; set; }  
    public string Email { get; set; }  
    public DateTime BirthDate { get; set; }  
    public string Phone { get; set; }  
    public string Address { get; set; }  
    public string City { get; set; }  
    public string State { get; set; }  
    public string PostalCode { get; set; }  
    public string ProfileUrl { get; set; } = string.Empty;  
  
    // Navigation property for the one-to-many relationship  
    public ICollection<Order> Orders { get; set; } = new List<Order>();  
}
public class Order  
{  
    public int Id { get; set; }  
    public DateTime OrderDate { get; set; }  
    public decimal TotalAmount { get; set; }  
    public string TrackingNumber { get; set; }  
  
    // Foreign key property to establish the one-to-many relationship  
    public int CustomerId { get; set; }  
  
    // Navigation property for the associated customer  
    public Customer Customer { get; set; }  
}

并低于我的客户 DTO

public class CustomerDto  
{  
    public string FirstName { get; set; } = string.Empty;  
    public string LastName { get; set; } = string.Empty;  
    public string Email { get; set; } = string.Empty;  
    public string Phone { get; set; } = string.Empty;  
    public int NumberOfOrders { get; set; }  
}

现在,我们的数据库已准备就绪,我们可以创建第一个用于检索客户的 API。

我们的客户控制器由一个 GET API 组成:

[ApiController]
[Route("[controller]")]
public class CustomersController : ControllerBase
{
    private readonly ApplicationDbContext _context;
    private readonly IMapper _mapper;
    private readonly HttpContext _httpContext;

    public CustomersController(ApplicationDbContext context, IMapper mapper, HttpContext httpContext)
    {
        _context = context;
        _mapper = mapper;
        _httpContext = httpContext;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetCustomerById(int id)
    {
           var customer = await _context.Customers
                .Include(x => x.Orders)
                .Where(x => x.Id == id)
                .FirstOrDefaultAsync();
        
           if (customer == null)
           {
               return NotFound();
           }
        
           var customerDto = _mapper.Map<Customer, CustomerDto>(customer);
        
           return Ok(customerDto);
    }
}

改进🔄空间

上述方法中有一个至关重要的问题。

首先,我们查询记录的所有数据,然后将其映射到我们的 DTO。第一个语句从数据库中提取特定客户。

EF 将生成以下 SQL 查询:

SELECT 
  -- All columns
  [t].[Id], [t].[Address], [t].[BirthDate], [t].[City], [t].[Email], [t].[FirstName], [t].[LastName], [t].[Phone], [t].[PostalCode], [t].[ProfileUrl], [t].[State], [o].[Id], [o].[CustomerId], [o].[OrderDate], [o].[TotalAmount], [o].[TrackingNumber]
FROM (
    SELECT TOP(1) [c].[Id], [c].[Address], [c].[BirthDate], [c].[City], [c].[Email], [c].[FirstName], [c].[LastName], [c].[Phone], [c].[PostalCode], [c].[ProfileUrl], [c].[State]
    FROM [Customers] AS [c]
    WHERE [c].[Id] = @__id_0
) AS [t]
LEFT JOIN [Orders] AS [o] ON [t].[Id] = [o].[CustomerId]
ORDER BY [t].[Id]

所有列都是从数据库中选择的,其中许多列在我们的用例中是不需要的。这对我们的应用意味着什么?它将需要分配更多的内存来容纳这些额外的信息字段。有人可能会争辩说,它可能没有那么重要;但是,在拥有数十万用户的实际应用程序中,它可能会对服务器的资源和响应能力产生负面影响。

为了解决这个问题,我们可以使用 Select 方法仅从数据库中选择必要的列,如下所示:

var customer = await _context.Customers  
    .Where(x => x.Id == id)  
    .Select(x => new CustomerDto  
    {  
        FirstName = x.FirstName,  
        LastName = x.LastName,  
        Email = x.Email,  
        Phone = x.Phone,  
        NumberOfOrders = x.Orders.Count  
    })  
    .FirstOrDefaultAsync();

现在,EF 将生成以下 SQL 查询:

SELECT TOP(1) [c].[FirstName], [c].[LastName], [c].[Email], [c].[Phone], (
          SELECT COUNT(*)
          FROM [Orders] AS [o]
          WHERE [c].[Id] = [o].[CustomerId]) AS [NumberOfOrders]
FROM [Customers] AS [c]
WHERE [c].[Id] = @__id_0

这是一个更节省内存的查询,因为我们选择的列更少。

将 AutoMapper 与 IQueryable 🤝 集成

我想知道是否有办法合并上述方法。因此,我深入研究了 AutoMapper 库的文档,并遇到了扩展方法(它正是这样做🎉的)。.ProjectTo

下面是使用该方法修改后的 API 端点。.ProjectTo

[HttpGet("{id}")]
public async Task<IActionResult> GetCustomerById(int id)
{
    var customerDto = await _context.Customers
        .Where(x => x.Id == id)
        .ProjectTo<CustomerDto>(_mapper.ConfigurationProvider)
        .FirstOrDefaultAsync();

    if (customerDto == null)
    {
        return NotFound();
    }

    return Ok(customerDto);
}

该方法有什么作用?.ProjectTo

根据官方文件:

它将告诉 AutoMapper 的映射引擎向 IQueryable 发出一个子句,该子句将通知实体框架它只需要查询 DTO 和表中存在的列,就像您手动将 your 投影到 a with a 子句一样。.ProjectTo<CustomerDto>(...)SelectIQueryableCustomerDtoSelect

因此,Entity Framework 将生成与以前完全相同的 SQL 查询。

技巧 💡

扩展方法也适用于存储库模式。与泛型结合使用,它可以简化和重用代码。

下面是一个简单的存储库,它公开了三种获取有关我们客户的不同数据的方法。

public class CustomerRepository : ICustomerRepository
{
    private readonly ApplicationDbContext _context;
    private readonly IMapper _mapper;

    public CustomerRepository(ApplicationDbContext context, IMapper mapper)
    {
        this._context = context;
        _mapper = mapper;
    }

    public async Task<CustomerDto> GetCustomerByIdAsync(int id)
    {
        return await _context.Customers
            .Where(x => x.Id == id)
            .ProjectTo<CustomerDto>(_mapper.ConfigurationProvider)
            .FirstOrDefaultAsync();
    }

    public async Task<CustomerWithOrdersDto> GetCustomerWithOrdersById(int id)
    {
        return await _context.Customers
           .Where(x => x.Id == id)
           .ProjectTo<CustomerWithOrdersDto>(_mapper.ConfigurationProvider)
           .FirstOrDefaultAsync();
    }

    public async Task<CustomerWithPendingOrdersDto> GetCustomerWithPendingOrdersById(int id)
    {
        return await _context.Customers
           .Where(x => x.Id == id)
           .ProjectTo<CustomerWithPendingOrdersDto>(_mapper.ConfigurationProvider)
           .FirstOrDefaultAsync();
    }
}

以上三种方法可以替换为:

public async Task<T> GetCustomerByIdAsync<T>(int id)
{
    return await _context.Customers
        .Where(x => x.Id == id)
        .ProjectTo<T>(_mapper.ConfigurationProvider)
        .FirstOrDefaultAsync();
}

并按以下方式使用:

//... Scenario 1  
var customer = await customerRepository.GetByIdAsync<CustomerDto>(id);  
  
//... Scenario 2  
var customerWithOrders = await customerRepository.GetByIdAsync<CustomerWithOrdersDto>(id);  
  
//... Scenario 3  
var customerWithPendingOrders = await customerRepository.GetByIdAsync<CustomerWithPendingOrdersDto>(id);

🙌 感谢您的阅读!我希望你觉得这篇文章有趣和有用。您对 AutoMapper 及其扩展方法有何经验?

相关留言评论
昵称:
邮箱:
阅读排行