十三、开始使用对象映射器

在本章中,我们将探讨对象映射。正如我们在上一章中所看到的,使用层通常会导致将模型从一个层复制到另一个层。对象映射器解决了这个问题。我们首先来看一下手动实现对象映射器。然后,我们将改进我们的设计。最后,我将向您介绍一个开源工具,它帮助我们生成业务价值,而不是编写映射代码。

本章将介绍以下主题:

  • 对象映射概述
  • 实现一个对象映射器,并探索几个备选方案
  • 使用服务定位器模式在我们的地图绘制者面前创建服务
  • 使用AutoMapper将一个对象映射到另一个对象,替换我们自制的代码

对象映射概述

什么是对象映射?简而言之,它是将一个对象的属性值复制到另一个对象的属性中的操作。但有时,属性的名称不匹配;对象层次可能需要展平,等等。正如我们在前一章中看到的,每个层都可以拥有自己的模型,这可能是一件好事,但这是以将对象从一层复制到另一层为代价的。我们也可以在层之间共享模型,但在某些时候我们需要某种映射。即使只是将您的模型映射到 DTO 或视图模型,这几乎是不可避免的,除非您正在构建一个小型应用,但即使如此,您可能想要或需要 DTO 和视图模型。

笔记

请记住,DTO 定义了 API 的契约。拥有独立的合同类可以帮助您维护系统,让您选择何时修改它们。如果跳过该部分,每次更改模型时,它都会自动更新端点的契约,可能会破坏某些客户端。此外,如果您直接输入模型,恶意用户可能会尝试绑定不应该绑定的属性值,从而导致潜在的安全问题。

在以前的项目中,我们手动实例化了发生转换的对象,复制了映射逻辑,并向进行映射的类添加了额外的职责。为了解决这个问题,我们将把映射逻辑提取到其他组件中。

目标

对象映射器的目标是将一个对象的属性值复制到另一个对象的属性中。它将映射逻辑封装在映射发生的位置之外。如果两个对象不遵循相同的结构,映射器还负责将值从原始格式转换为目标格式。例如,我们可以将对象层次结构展平。

设计

我们可以以多种方式进行设计,但我们可以用以下方式表示基本设计:

Figure 13.1 – Basic design of the object mapper

图 13.1–对象映射器的基本设计

从图中,消费者使用IMapper接口将Type1的对象映射到Type2的对象。这不是非常可重用的,但它说明了这个概念。通过使用泛型的强大功能,我们可以将简单的设计升级到更可重用的版本:

Figure 13.2 – Updating the design of the object pattern

图 13.2–更新对象模式的设计

我们现在可以实现IMapper<TSource, TDestination>接口,并为每个映射规则创建一个类,或者创建一个实现多个映射规则的类。例如,我们可以在同一个类中实现Type1Type2Type2Type1的映射。

我们还可以使用以下设计并使用一个方法创建IMapper接口,该方法处理应用的所有映射:

图 13.3–使用单个 IMapper 作为入口点的对象映射

最后一种设计的最大优点是易于使用。我们总是为每种类型的映射注入一个IMapper而不是一个IMapper<TSource, TDestination>,这将减少依赖项的数量和使用这种映射器的复杂性。最大的缺点可能是实现的复杂性,但正如我们即将发现的那样,我们可以从等式中去掉这一点。

您可以使用想象中允许的任何方式实现对象映射,但需要记住的关键部分是映射者有责任将一个对象映射到另一个对象。映射程序不应该做一些疯狂的事情,比如从数据库加载数据等等。它应该将一个对象的值复制到另一个对象中;就这样。

让我们跳转到一些代码中,以更深入地探索每个项目的设计。

项目:Mapper

此项目是上一章中干净架构代码的更新版本,其中包含数据模型域模型。我使用该版本展示了数据到域和域到 DTO 的映射。如果您不需要数据模型,请不要创建数据模型,尤其是使用干净的体系结构。尽管如此,我们的目标是展示设计的多功能性,并将实体映射封装到映射器类中,以从存储库和控制器中提取该逻辑。

首先,我们需要一个驻留在Core项目中的接口,以便其他项目可以实现它们需要的映射。让我们采用我们看到的第二种设计:

namespace Core.Interfaces
{
    public interface IMapper<TSource, TDestination>
    {
        TDestination Map(TSource entity);
    }
}

通过该接口,我们可以从创建数据映射器开始。因为我们正在将Data.Models.Product映射到Core.Entities.Product,反之亦然,所以我选择了一个类来实现这两个映射:

namespace Infrastructure.Data.Mappers
{
    public class ProductMapper : IMapper<Data.Models.Product, Core.Entities.Product>, IMapper<Core.Entities.Product, Data.Models.Product>
    {
        public Core.Entities.Product Map(Data.Models.Product entity)
        {
            return new Core.Entities.Product
            {
                Id = entity.Id,
                Name = entity.Name,
                QuantityInStock = entity.QuantityInStock
            };
        }
        public Data.Models.Product Map(Core.Entities.Product entity)
        {
            return new Data.Models.Product
            {
                Id = entity.Id,
                Name = entity.Name,
                QuantityInStock = entity.QuantityInStock
            };
        }
    }
}

这些是简单的方法,可以将一个对象的属性值复制到另一个对象中。尽管如此,现在我们已经完成了,我们准备更新ProductRepository类以使用新的映射器:

namespace Infrastructure.Data.Repositories
{
    public class ProductRepository : IProductRepository
    {
        private readonly ProductContext _db;
        private readonly IMapper<Data.Models.Product, Core.Entities.Product> _dataToEntityMapper;
 private readonly IMapper<Core.Entities.Product, Data.Models.Product> _entityToDataMapper;
        public ProductRepository(ProductContext db, IMapper<Data.Models.Product, Core.Entities.Product> productMapper, IMapper<Core.Entities.Product, Data.Models.Product> entityToDataMapper)
        {
            _db = db ?? throw new ArgumentNullException(nameof(db));
            _dataToEntityMapper = productMapper ?? throw new ArgumentNullException (nameof(productMapper));
 _entityToDataMapper = entityToDataMapper ?? throw new ArgumentNullException (nameof(entityToDataMapper));
        }

在前面的代码中,我们已经注入了所需的两个映射器。即使我们创建了一个实现两个接口的类,使用者(ProductRepository)也不知道这一点。接下来是使用映射器的方法:

        public IEnumerable<Core.Entities.Product> All()
        {
            return _db.Products.Select(p => _dataToEntityMapper.Map(p));
        }
        public void DeleteById(int productId)
        {
            var product = _db.Products.Find(productId);
            _db.Products.Remove(product);
            _db.SaveChanges();
        }
        public Core.Entities.Product FindById(int productId)
        {
            var product = _db.Products.Find(productId);
            return _dataToEntityMapper.Map(product);
        }
        public void Insert(Core.Entities.Product product)
        {
            var data = _entityToDataMapper.Map(product);
            _db.Products.Add(data);
            _db.SaveChanges();
        }
        public void Update(Core.Entities.Product product)
        {
            var data = _db.Products.Find(product.Id);
            data.Name = product.Name;
            data.QuantityInStock = product.QuantityInStock;
            _db.SaveChanges();
        }
    }
}

然后,类使用映射器替换其复制逻辑(前面突出显示的行)。这简化了存储库,将映射责任转移到 mapper 对象中,而不是向单一责任原则(SRP–实体中的“S”)迈进了一步。

然后将相同的原理应用于 web 应用,创建ProductMapperStockMapper,并更新控制器以使用它们。为了简洁起见,我省略了代码,但基本上是一样的。请看 GitHub(上的代码 https://net5.link/rZdN )。

唯一缺少的部分是 compositionroot,在这里我们将映射器实现与IMapper<TSource, TDestination>接口绑定。数据绑定如下所示:

services.AddSingleton<IMapper<Infrastructure.Data.Models.Product, Core.Entities.Product>, Infrastructure.Data.Mappers.ProductMapper>();
services.AddSingleton<IMapper<Core.Entities.Product, Infrastructure.Data.Models.Product>, Infrastructure.Data.Mappers.ProductMapper>();

因为ProductMapper实现了两个接口,所以我们将它们都绑定到该类。这是抽象的美之一;ProductRepository请求两个映射器,但两次收到相同的ProductMapper实例,甚至都不知道。

笔记

是的,我是故意的。这证明我们可以按照自己的意愿编写应用,而不会影响消费者。根据依赖倒置原则,这是通过依赖抽象而不是具体化来实现的。此外,根据接口隔离原则(ISP–实体中的“I”)将接口划分为小接口,这使得这种情况成为可能。最后,使用依赖注入DI的力量将所有这些片段重新组合在一起。

我希望你们开始明白我的想法,因为我们正在把越来越多的东西放在一起。

代码气味:依赖项太多

从长远来看,使用这种映射可能会变得单调乏味,我们会很快看到这样的场景,比如将三个或更多映射器注入到一个控制器中。该控制器很可能已经具有其他依赖项,从而导致四个或更多依赖项。

这将升起以下旗帜:

  • 那班学生是否做得太多,承担的责任太多?

在这种情况下,不是真的,但是我们的细粒度接口会污染我们的控制器,使其对映射器产生大量依赖,这是不理想的,并且使我们的代码更难阅读。

如果你好奇的话,下面是我是如何想出第三个数字的:

  • EntityDTOGetOneGetAllInsertUpdate、可能还有Delete的映射)
  • DTOEntityInsert的映射
  • DTOEntityUpdate的映射

根据经验,您希望将依赖项的数量限制为三个或更少。超过这个数字,问问你自己,这门课是否有问题;它是否有太多的责任?拥有四个以上的依赖关系本身并不坏;这只是一个指标,你应该重新考虑设计的某些部分。如果没有问题,保持在 4、5 或 10;没关系。

如果您不希望有那么多依赖项,那么可以提取封装两个或多个依赖项的服务聚合,然后注入该聚合。请注意,移动依赖项并不能解决任何问题;如果一开始有问题,它只是把问题转移到别处。不过,使用聚合可以提高代码的可读性。

不要盲目地移动依赖项,而是分析问题,看看是否可以创建具有实际逻辑的类,这些逻辑可以做一些有用的事情来减少依赖项的数量。

模式-聚合服务

即使聚合服务不是一种神奇的问题解决模式,它也是向另一个类注入大量依赖项的可行替代方案。它的目标是聚合另一个类中的许多依赖项,以减少其他类中注入的服务的数量,将依赖项分组在一起。管理总量的方法是按关注点或责任对这些问题进行分组。仅仅为了另一个服务而将一堆公开的服务放在另一个服务中很少是可行的;以凝聚为目标。

笔记

创建一个或多个公开其他服务的中央聚合服务是在项目中实现服务(接口)发现的一种好方法。我不是要你把所有的东西都直接放在一起。然而,如果服务的发现是您项目中的一个问题,或者您和您的团队发现它很困难,那么这是一个值得研究的可能性。

下面是一个假设映射聚合的示例,用于减少我们假想的 CRUD 控制器的依赖性:

public interface IProductMappers
{
    IMapper<Product, ProductDetails> EntityToDto { get; }
    IMapper<InsertProduct, Product> InsertDtoToEntity { get; }
    IMapper<UpdateProduct, Product> UpdateDtoToEntity { get; }
}
public class ProductMappers : IProductMappers
{
    public ProductMappers(IMapper<Product, ProductDetails> entityToDto, IMapper<InsertProduct, Product> insertDtoToEntity, IMapper<UpdateProduct, Product> updateDtoToEntity)
    {
        EntityToDto = entityToDto ?? throw new ArgumentNullException(nameof(entityToDto));
        InsertDtoToEntity = insertDtoToEntity ?? throw new ArgumentNullException(nameof(insertDtoToEntity));
        UpdateDtoToEntity = updateDtoToEntity ?? throw new ArgumentNullException(nameof(updateDtoToEntity));
    }
    public IMapper<Product, ProductDetails> EntityToDto { get; }
    public IMapper<InsertProduct, Product> InsertDtoToEntity { get; }
    public IMapper<UpdateProduct, Product> UpdateDtoToEntity { get; }
}
public class ProductsController : ControllerBase
{
    private readonly IProductMappers _mapper;
    // …
    public ProductDetails Method()
    {
        var product = default(Product);
        var dto = _mapper.EntityToDto.Map(product);
        return dto;
    }
}

从这个示例中,IProductMappers聚合可以理解为它重新组合了ProductsController类中使用的所有映射器。它只负责将与ProductsController相关的域对象映射到 DTO,反之亦然。您可以使用任何东西创建聚合,而不仅仅是映射器。这是 DI 密集型应用中相当常见的模式。

笔记

只要聚合服务不可能更改并且不实现任何逻辑,我们就可以省略接口并直接注入具体类型。因为我们在这里主要关注坚实的原则,所以我决定包括接口(这本身并不是一件坏事)。没有接口的一个优点是,使用混凝土类型可以降低单元测试中模拟聚合的复杂性。只要你不试着把逻辑放进去,我看不出有什么缺点。

图案-映射立面

我们可以创建一个映射 façade 而不是聚合,而不是在前面的案例中所做的。使用立面的代码更加优雅。façade 的职责与聚合相同,但它实现了接口,而不是公开属性。

以下是一个例子:

public interface IProductMapperService : IMapper<Product, ProductDetails>, IMapper<InsertProduct, Product>, IMapper<UpdateProduct, Product>
{
}
public class ProductMapperService : IProductMapperService
{
    private readonly IMapper<Product, ProductDetails> _entityToDto;
    private readonly IMapper<InsertProduct, Product> _insertDtoToEntity;
    private readonly IMapper<UpdateProduct, Product> _updateDtoToEntity;
    // ...
    public ProductDetails Map(Product entity)
    {
        return _entityToDto.Map(entity);
    }
    public Product Map(InsertProduct dto)
    {
        return _insertDtoToEntity.Map(dto);
    }
    public Product Map(UpdateProduct dto)
    {
        return _updateDtoToEntity.Map(dto);
    }
}
public class ProductsController : ControllerBase
{
    private readonly IProductMapperService _mapper;
    // ...
    public ProductDetails Method()
    {
        var product = default(Product);
        var dto = _mapper.Map(product);
        return dto;
    }
}

ProductsController的角度来看,我总是觉得写_mapper.Map(…)而不是_mapper.SomeMapper.Map(…)更干净。控制器不想知道什么映射器正在进行什么映射;它唯一想要的就是映射需要映射的内容。

现在,我们已经介绍了一些映射选项,并探讨了太多依赖项代码的味道,现在是时候继续我们的对象映射之旅了,我们将使用“基于类固醇的映射外观”

项目-测绘服务

目标是使用通用接口简化 mapper façade 的实现。

我们将使用第三个图表来实现这一目标。这里有一个提醒:

Figure 13.4 – Object mapping using a single IMapper interface

图 13.4–使用单个 IMapper 接口的对象映射

我没有将接口命名为IMapper,而是发现IMappingService更合适,因为它没有映射任何东西;它是一个调度器,将映射请求发送给正确的映射程序。让我们来看一看:

namespace Core.Interfaces
{
    public interface IMappingService
    {
        TDestination Map<TSource, TDestination>(TSource entity);
    }
}

这个界面是不言自明的;它将任意TSource映射到任意TDestination

在实现端,我们正在利用服务定位器模式,所以我将实现称为ServiceLocatorMappingService

namespace Web.Services
{
    public class ServiceLocatorMappingService : IMappingService
    {
        private readonly IServiceProvider _serviceProvider;
        public ServiceLocatorMappingService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
        }
        public TDestination Map<TSource, TDestination>(TSource entity)
        {
            var mapper = _serviceProvider.GetService<IMapper<TSource, TDestination>>();
            if (mapper == null)
            {
                throw new MapperNotFoundException (typeof(TSource), typeof(TDestination));
            }
            return mapper.Map(entity);
        }
    }
}

逻辑很简单:

  • 找到合适的IMapper<TSource, TDestination> 服务,然后用它映射实体。
  • 如果你找不到,扔一个MapperNotFoundException

设计的关键是将映射器注册到 IoC 容器中,而不是服务本身。然后,我们使用映射器,而不知道它们中的每一个,如前一个示例中所示。ServiceLocatorMappingService类不知道任何映射器;它只是在需要时动态地请求一个。

提示

我不太喜欢应用代码中的服务定位器模式。服务定位器是一种代码气味,有时甚至更糟,是一种反模式。然而,有时它会派上用场,就像在这种情况下一样。我们并不是在试图欺骗依赖注入;相反,我们利用了它的力量。此外,该服务位置需要在某处完成。通常,我更喜欢让框架为我做这件事,但在本例中,我们明确地做了,这很好。

当以某种方式获取依赖项时,服务定位器的使用是错误的,这种方式消除了从组合根控制程序组合的可能性。这将违反国际奥委会的原则。

在这种情况下,我们从 IoC 容器动态加载映射器,但是这限制了容器控制注入内容的能力,但是对于这种类型的实现来说,这是可以接受的。

现在,我们可以在需要映射的任何地方注入该服务,然后直接使用它。我们已经注册了映射程序,所以我们只需要将IMappingService绑定到其ServiceLocatorMappingService实现并更新使用者。

如果我们看一下ProductRepository的新版本,我们现在有以下内容:

namespace Infrastructure.Data.Repositories
{
    public class ProductRepository : IProductRepository
    {
        private readonly ProductContext _db;
 private readonly IMappingService _mappingService;
        public ProductRepository(ProductContext db, IMappingService mappingService)
        {
            _db = db ?? throw new ArgumentNullException(nameof(db));
            _mappingService = mappingService ?? throw new ArgumentNullException(nameof(mappingService));
        }

这里,与上一个示例不同,我们注入了单个服务,而不是每个映射器注入一个服务:

        public IEnumerable<Product> All()
        {
            return _db.Products.Select(p => _mappingService.Map<Models.Product, Product>(p));
        }
        // ...
        public Product FindById(int productId)
        {
            var product = _db.Products.Find(productId);
            return _mappingService.Map<Models.Product, Product>(product);
        }
        public void Insert(Product product)
        {
            var data = _mappingService.Map<Product, Models.Product>(product);
            _db.Products.Add(data);
            _db.SaveChanges();
        }
        //...
    }
}

这与之前的示例非常相似,但我们用新服务(突出显示的行)替换了映射器。最后一件是 DI 装订:

services.AddSingleton<IMappingService, ServiceLocatorMappingService>();

就这样;我们现在有了一个通用映射服务,它将映射委托给我们在 IoC 容器中注册的任何映射器。

笔记

我使用单态生存期是因为ServiceLocatorMappingService 没有状态;每次都可以重用它,而不会对映射逻辑产生任何影响。

最好的部分是,这还不是对象映射的最高点。我们有一个工具要探索,名为 AutoMapper。

项目-汽车制造商

我们刚刚介绍了实现对象映射的不同方法,但这里我们将利用一个名为 AutoMapper 的开源工具,它为我们实现对象映射,而不是我们自己实现对象映射。

如果有一个工具已经做到了这一点,为什么还要费心去学习这些呢?这样做有几个原因:

  • 理解这些概念很重要;您并不总是需要像 AutoMapper 这样的成熟工具。
  • 它使我们有机会涵盖我们应用于映射器的多种模式,这些模式也可以应用于其他任何具有不同职责的组件。总之,在这个对象映射过程中,您应该已经学习了多种新技术。
  • 最后,我们更深入地研究了应用坚实的原则来编写更好的程序。

该项目也是 Clean 架构示例的副本。这个项目与其他项目最大的区别在于,我们不需要定义任何接口,因为 AutoMapper 公开了一个包含我们需要的所有方法的IMapper接口,等等。

要安装 AutoMapper,您可以使用 CLI(dotnet add package AutoMapper)、Visual Studio 的 NuGet 软件包管理器或手动更新您的.csproj来加载AutoMapperNuGet 软件包。

定义映射器的最佳方法是使用 AutoMapper 的配置文件机制。概要文件是从AutoMapper.Profile继承的简单类,包含从一个对象到另一个对象的映射。我们可以使用与前面类似的分组,但不实现接口。

最后,我们可以使用AutoMapper.Extensions.Microsoft.DependencyInjection包扫描一个或多个程序集,将所有配置文件加载到 AutoMapper 中,而不是手动注册配置文件。

AutoMapper 的功能远不止这些,但它有足够的在线资源,包括官方文档,帮助您深入挖掘该工具。

基础设施项目中,我们需要将Data.Models.Product映射到Entities.Product,反之亦然。我们可以在我们命名为ProductProfile的配置文件中实现这一点:

namespace Infrastructure.Data.Mappers
{
    public class ProductProfile : Profile
    {
        public ProductProfile()
        {
            CreateMap<Data.Models.Product, Core.Entities.Product>().ReverseMap();
        }
    }
}

AutoMapper 中的配置文件只是一个类,您可以在构造函数中创建映射。Profile类为您添加了所需的方法,例如CreateMap方法。那有什么用?

CreateMap<Data.Models.Product, Core.Entities.Product>()告诉 AutoMapper 注册一个将Data.Models.Product映射到Core.Entities.Product的映射器。然后ReverseMap()方法告诉 AutoMapper 反转该地图,因此从Core.Entities.ProductData.Models.Product。这就是我们现在所需要的,因为 AutoMapper 使用约定映射属性,并且我们的两个类都具有相同名称的相同属性集。

Web项目的角度来看,我们也需要一些映射器,我将其分为以下两个配置文件:

namespace Web.Mappers
{
    public class StocksProfile : Profile
    {
        public StocksProfile()
        {
            CreateMap<Product, StocksController.StockLevel>();
        }
    }
    public class ProductProfile : Profile
    {
        public ProductProfile()
        {
            CreateMap<Product, ProductsController.ProductDetails>();
        }
    }
}

我们本可以将它们合并为一个,但我决定不合并,因为我觉得每个控制器一个配置文件是有意义的,尤其是随着应用的增长。

下面是一个示例,说明了单个配置文件中的多个贴图:

namespace Web.Mappers
{
    public class ProductProfile : Profile
    {
        public ProductProfile()
        {
            CreateMap<Product, ProductsController.ProductDetails>();
            CreateMap<Product, StocksController.StockLevel>();
        }
    }
}

要扫描来自合成根的概要文件,我们可以使用AddAutoMapper扩展方法之一(来自AutoMapper.Extensions.Microsoft.DependencyInjection包):

services.AddAutoMapper(
    GetType().Assembly,
    typeof(Infrastructure.Data. Mappers.ProductProfile).Assembly
);

该方法接受一个params Assembly[] assemblies参数,这意味着我们可以向它传递一个数组或多个Assembly实例。

第一个AssemblyWeb组件,通过调用GetType().AssemblyStartup类获取(该代码在Startup.ConfigureServices方法中)。从那里,AutoMapper 应该可以找到StocksProfileProductProfile类。

第二个Assembly基础设施组件,使用Data.Mappers.ProductProfile类获取。需要注意的是,该组件中的任何类型都会为我们提供关于Infrastructure组件的参考;不需要找到继承自Profile的类。从那里,AutoMapper 应该可以找到Data.Mappers.ProductProfile类。

扫描像这样的类型的好处在于,一旦您向 IoC 容器注册 AutoMapper,您就可以在任何已注册的程序集中添加配置文件,它们会自动加载;除了编写有用的代码,以后不需要做任何其他事情。扫描组件也鼓励按照惯例进行组合,从而使其更易于长期维护。程序集扫描的缺点是,当某些内容未注册时,很难进行调试。也很难发现注册模块有什么问题。

现在我们已经创建了配置文件并在 IoC 容器中注册了它们,现在是使用 AutoMapper 的时候了。让我们来看看 AutoT0.类:

namespace Infrastructure.Data.Repositories
{
    public class ProductRepository : IProductRepository
    {
        private readonly ProductContext _db;
        private readonly IMapper _mapper;
        public ProductRepository(ProductContext db, IMapper mapper)
        {
            _db = db ?? throw new ArgumentNullException(nameof(db));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
        }

在前面的代码中,我们注入了 AutoMapper 的IMapper接口。

        public IEnumerable<Product> All()
        {
#if USE_PROJECT_TO
            // Transposed to a Select() possibly optimal in some cases; previously known as "Queryable Extensions".
            return _mapper.ProjectTo<Product>(_db.Products);
#else
            // Manual Mapping (query the whole object, then map it; could lead to "over-querying" the database)
            return _db.Products.Select(p => _mapper.Map<Product>(p));
#endif
        }

All方法(前面的代码块)公开了我后面描述的两种映射集合的方法。接下来是另外两个更新的方法:

        // ...
        public Product FindById(int productId)
        {
            var product = _db.Products.Find(productId);
            return _mapper.Map<Product>(product);
        }
        public void Insert(Product product)
        {
            var data = _mapper.Map<Models.Product>(product);
            _db.Products.Add(data);
            _db.SaveChanges();
        }
        // ...
    }
}

正如你所看到的,它与其他选项非常相似;我们注入一个IMapper,然后使用它映射实体。唯一代码多一点的方法是All()方法。原因是我添加了两种方法来映射集合。

第一种方式是使用IQueryable接口限制查询字段数量的ProjectTo<TDestination>()方法。在我们的例子中,这不会改变什么,因为我们需要整个实体。建议 EF 使用该方法。

All()方法应简单如下:

public IEnumerable<Product> All()
{
    return _mapper.ProjectTo<Product>(_db.Products);
}

第二种方法直接使用Map方法,正如我们在实现中所做的那样,并在投影中使用,如下所示:

public IEnumerable<Product> All()
{
    return _db.Products.Select(p => _mapper.Map<Product>(p));
}

所有其他案例都非常简单,使用 AutoMapper 的Map方法。两个控制器执行相同的操作,如下所示:

namespace Web.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ProductsController : ControllerBase
    {
        private readonly IProductRepository _productRepository;
        private readonly IMapper _mapper;
        public ProductsController(IProductRepository productRepository, IMapper mapper)
        {
            _productRepository = productRepository ?? throw new ArgumentNullException (nameof(productRepository));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
        }
        [HttpGet]
        public ActionResult<IEnumerable<ProductDetails>> Get()
        {
            var products = _productRepository
                .All()
                .Select(p => _mapper.Map<ProductDetails>(p));
            return Ok(products);
        }
        // ...
    }

下面是使用AutoMapper将域实体映射到 DTO 的StocksController(突出显示的行):

    [ApiController]
    [Route("products/{productId}/")]
    public class StocksController : ControllerBase
    {
 private readonly IMapper _mapper;
        public StocksController(IMapper mapper)
        {
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
        }
        [HttpPost("add-stocks")]
        public ActionResult<StockLevel> Add(
            int productId,
            [FromBody] AddStocksCommand command,
            [FromServices] AddStocks useCase
        )
        {
            var product = useCase.Handle(productId, command.Amount);
            var stockLevel = _mapper.Map<StockLevel>(product);
            return Ok(stockLevel);
        }
        [HttpPost("remove-stocks")]
        public ActionResult<StockLevel> Remove(
            int productId,
            [FromBody] RemoveStocksCommand command,
            [FromServices] RemoveStocks useCase
        )
        {
            try
            {
                var product = useCase.Handle(productId, command.Amount);
                var stockLevel = _mapper.Map<StockLevel>(product);
                return Ok(stockLevel);
            }
            catch (NotEnoughStockException ex)
            {
                return Conflict(new
                {
                    ex.Message,
                    ex.AmountToRemove,
                    ex.QuantityInStock
                });
            }
        }
        // ...
    }
}

我想补充的最后一个细节是,我们可以在应用启动时断言映射器配置是否有效。这不会指向缺少映射器,但会验证已注册映射器的配置是否正确。在Startup类中,您可以编写以下代码:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IMapper mapper)
{
    // ...
    mapper.ConfigurationProvider.AssertConfigurationIsValid();
    // ...
}

这就结束了汽车制造商项目。此时,您应该开始熟悉对象映射。

我建议您在项目需要对象映射时,评估 AutoMapper 是否是适合该工作的工具。如果不是,则始终可以加载其他工具或实现自己的映射逻辑。

最后说明

AutoMapper 是基于约定的,它自己做很多事情,而不需要我们开发人员的任何配置。它还基于配置,缓存转换以提高性能。我们还可以创建型转换器、值分解器、值转换器、等。AutoMapper 让我们远离编写无聊的映射代码,我还没有找到更好的工具。

总结

在大多数情况下,对象映射是可以避免的现实。但是,正如我们在本章中所看到的,有几种实现对象映射的方法,将这种责任从应用的其他组件中移开。其中一种方法是 AutoMapper,这是一种为我们提供的开源工具,它为我们提供了许多配置对象映射的选项。

现在让我们看看对象映射如何帮助我们遵循实体原则:

  • S:它有助于从其他类中提取映射责任,将映射逻辑封装到映射器对象或自动映射配置文件中。
  • O:通过注入映射器,我们可以更改映射逻辑,而无需更改其使用者的代码。
  • L:不适用。
  • I:我们看到了将映射程序划分为更小接口的不同方法。AutoMapper 也不例外;它公开了IMapper接口,并在引擎盖下使用其他接口和实现。
  • D:我们所有的代码都只依赖于接口,将实现的绑定移动到组合根。

现在我们已经完成了对象映射,在下一章中,我们将探索中介体CQRS模式。然后,我们将结合我们的知识学习一种新的应用级体系结构,名为垂直切片体系结构

问题

让我们来看看几个练习题:

  1. 注入一个聚合服务而不是多个服务真的会让我们的系统变得更好吗?
  2. 使用映射器帮助我们将责任从消费者提取到映射器类,这是真的吗?
  3. 您是否应该始终使用 AutoMapper?
  4. 当使用 AutoMapper 时,是否应该将映射代码封装到配置文件中?
  5. 有多少依赖项应该开始升起一个标志,告诉您将太多的依赖项注入到一个类中?

进一步阅读

以下是我们在本章学到的一些链接:

  • 如果你想要更多的对象映射,我在 2017 年写了一篇文章,题目是设计模式:ASP.NET Core Web API、服务和存储库【第 9 部分:NinjaMappingService 和 Façade 模式https://net5.link/hxYf
  • AutoMapper 官方网站:https://net5.link/5AUZ
  • AutoMapper 使用指南是一个很好的做/不做列表,帮助您正确使用 AutoMapper,由图书馆作者撰写:https://net5.link/tTKg