2.7 ABP公共结构 - 对象之间的映射

2.7.1 简介

我们通常需要在近似的对象之间进行映射处理。这是一个重复且枯燥无味的工作,通常来说两个需要相互映射的对象之间有近似的或者相同的属性。思考一下这样一个案例:应用服务的方法:

public class UserAppService : ApplicationService
{
    private readonly IRepository<User> _userRepository;

    public UserAppService(IRepository<User> userRepository)
    {
        _userRepository = userRepository;
    }

    public void CreateUser(CreateUserInput input)
    {
        var user = new User
        {
            Name = input.Name,
            Surname = input.Surname,
            EmailAddress = input.EmailAddress,
            Password = input.Password
        };

        _userRepository.Insert(user);
    }
}

在这里,User是一个简单的实体;CreateUserInput是一个简单的DTO。从给定的输入对象,我们需要使用它来创建一个User实体。在真实的环境中User实体会有更多的属性,手动创建这个实体会变得枯燥无味且易出错。如果我们想要添加新的属性到User和CreateUserInput的时候,这会变得很复杂,我们应该改变这个映射代码,使映射更简单。

我们可以使用一个类库来实现自动映射。AutoMapper是最好的处理对象到对象之间映射的类库之一。ABP中定义了 IObjectMapper 接口,抽象了映射功能。在Abp.AutoMapper包中,我们实现了该接口来使用AutoMapper。

2.7.2 IObjectMapper 接口

IObjectMapper简单的抽象出了对象到对象之间映射的方法。我们可以使用更简单的代码实现上面提到的映射功能:

public class UserAppService : ApplicationService
{
    private readonly IRepository<User> _userRepository;
    private readonly IObjectMapper _objectMapper;

    public UserAppService(IRepository<User> userRepository, IObjectMapper objectMapper)
    {
        _userRepository = userRepository;
        _objectMapper = objectMapper;
    }

    public void CreateUser(CreateUserInput input)
    {
        var user = _objectMapper.Map<User>(input);
        _userRepository.Insert(user);
    }
}

Map 是一个简单的具有类型声明的泛型占位符的方法,可以将一个对象映射为另一个对象。Map方法的重载方法可以映射一个对象到一个 已存在 的对象。假设我们有了一个User实体,但是我们想通过DTO来更新用户实体的某些属性:

public void UpdateUser(UpdateUserInput input)
{
    var user = _userRepository.Get(input.Id);
    _objectMapper.Map(input, user);
}

2.7.3 AutoMapper 集成

Abp.AutoMapper包中,我们实现了IObjectMapper接口并提供了一些辅助功能。

安装

首先,需要安装 Abp.AutoMapper 到你的项目中:

Install-Package Abp.AutoMapper

然后添加 AbpAutoMapperModule 作为依赖项到你定义的模块类中:

[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{
    ...
}

这样你就可以在代码中安全的注入和使用IObjectMapper接口了。如果有需要,你也可以使用AutoMapper自己的API。

创建映射

在使用映射之前,AutoMapper默认需要定义类之间的映射关系。在使用的时候你可以查询它的文档。但是使用ABP会使映射关系的创建更简单且模块化。

自动映射特性

大多数时候你只想对类进行直接(按约定的方式)映射。在这种情况下,你可以使用 AutoMap,AutoMapFrom 以及 AutoMapTo 特性。例如:在上面的例子中,我们将 CreateUserInput 映射到 User,我们可以使用 AutoMapTo 特性来实现。如下所示:

[AutoMapTo(typeof(User))]
public class CreateUserInput
{
    public string Name { get; set; }

    public string Surname { get; set; }

    public string EmailAddress { get; set; }

    public string Password { get; set; }
}

AutoMap特性可以在两个类型之间实现彼此之间的相互映射。但是在这个例子中,我们只需要将 CreateUserInput 映射到 User。所以我们可以使用 AutoMapTo

自定义映射

在某些情况下,简单的映射不能满足需求。例如:两个类中的属性名字可能稍微有些不同或者你想忽略某些属性的映射。在这种情况下,你可以直接使用 AutoMapper 的 API 来实现映射。Abp.AutoMapper 包中的定义的 API 使自定义映射更加模块化。

假设在映射的时候,我们想忽略Password属性,并使 EmailAddress 属性映射到 User 的Email 属性。我们可以像下面一样来实现映射关系:

[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{
    public override void PreInitialize()
    {
        Configuration.Modules.AbpAutoMapper().Configurators.Add(config =>
        {
            config.CreateMap<CreateUserInput, User>()
                  .ForMember(u => u.Password, options => options.Ignore())
                  .ForMember(u => u.Email, options => options.MapFrom(input => input.EmailAddress));
        });
    }
}

AutoMapper拥有更多的选项和能力来做对象之间的映射。详情请查询文档

MapTo扩展方法

如上面所述,我们建议注入并使用IObjectMapper接口。这使我们的项目尽可能的不依赖AutoMapper。这也使单元测试更简单,因为在单元测试的时候我们可以替换掉映射依赖。

在 Abp.AutoMapper 中也有 MapTo 的扩展方法,我们可以不注入IObjectMapper接口,使用它将任意对象映射为其它对象。如下所示:

public class UserAppService : ApplicationService
{
    private readonly IRepository<User> _userRepository;

    public UserAppService(IRepository<User> userRepository)
    {
        _userRepository = userRepository;
    }

    public void CreateUser(CreateUserInput input)
    {
        var user = input.MapTo<User>();
        _userRepository.Insert(user);
    }

    public void UpdateUser(UpdateUserInput input)
    {
        var user = _userRepository.Get(input.Id);
        input.MapTo(user);
    }
}

在 Abp.AutoMapper 的名称空间中定义了 MapTo 的扩展方法。首先你得在你的代码中导入该名称空间。

由于MapTo扩展方法是静态的,它使用的是AutoMapper的静态实例。对于应用程序代码这是简单且有效的,但是静态配置在单元测试的时候会有问题,因为在单元测试的时候,会在各个单元测试之间共享映射关系。

单元测试

我们想隔离各个测试单元。为了实现该功能,我们应该使我们的项目遵循下面规则:

    1. 使用IObjectMapper接口,而不使用MapTo静态方法。
    1. 配置 Abp.AutoMapper 模块使用本地Mapper实例(在依赖注入时,注册为单例模式)而不是静态实例(Abp.AutoMapper 默认使用静态 Mapper.Instance实例,这样我们就可以使用MapTo扩展方法来实现对象之间的映射,如上面例子所示)。
Configuration.Modules.AbpAutoMapper().UseStaticMapper = false;

预定义映射

LocalizableString → string

Abp.AutoMapper模块中定义了一个映射方法:将LocalizableString(或者ILocalizableString)对象转换为string对象。它使用 ILocalizationManager接口来实现转换。所以在处理任意类的映射时,可本地化属性被自动的本地化。

注入IMapper

你可以直接使用AutoMapper的IMapper对象,而不是使用IObjectMapper接口。这样的话,在你的类中注入IMapper并使用它。Abp.AutoMapper包注册IMapper作为依赖注入单例。