ABP官方文档翻译 2.7 对象到对象的映射
对象到对象的映射
映射一个相似的对象到另一个对象是常有的事情。因为两个对象(类)有相似/相同的属性,所以两个相似对象之间相互映射是乏味、重复的。考虑一个经典的应用服务方法如下:
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); } }
CreateUserInput是一个简单的DTO,User是一个简单的实体。我们根据给定的input手动创建了一个User实体。在实际的应用中,用户实体会有许多的属性,手动创建它变得乏味且容易出错的。当我们想给User和CreateUserInput添加新属性时,需要改变映射代码。
我们可以使用一个类库来自动映射。AutoMapper是对象到对象映射最好的类库之一。ABP定义了IObjectMapper接口进行抽象,并在Abp.AutoMapper包里使用AutoMapper实现了这个接口。
IObjectMapper是一个简单的抽象,它包含Map方法用来映射一个对象到另一个。我们可以把上面的实例代码修改如下:
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是一个简单的方法,获取源对象并使用声明的泛型参数(本例中为User)创建一个新目标对象。Map方法有一个重载,可以映射一个对象到一个已存在的对象。假定我们已经有了一个User实体,想使用一个对象更新它的属性:
public void UpdateUser(UpdateUserInput input) { var user = _userRepository.Get(input.Id); _objectMapper.Map(input, user); }
Abp.AutoMapper nuget包(模块)实现了IObjectMapper且提供了额外的特征。
首先,在工程中安装Abp.AutoMapper包:
Install-Package Abp.AutoMapper
然后,添加AbpAutoMapperModule依赖到模块定义类中:
[DependsOn(typeof(AbpAutoMapperModule))] public class MyModule : AbpModule { ... }
然后就可以在代码里安全的注入和使用IObjectMapper。当需要的时候,也可以使用AutoMapper自己的API。
AutoMapper需要定义一个类之间的映射在使用它之前。可以参见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,用户用Email属性来标示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有许多选项和功能用来映射对象。参见文档了解更多。
推荐注入并使用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); } }
MapTo扩展方法定义在Abp.AutoMapper命名空间,所以需要首先在代码文件里导入这个命名空间。
因为MapTo扩展方法是静态的,他们使用AutoMapper的静态实例(Mapper.Instance)。对应用代码来说,这是简单且友好的,但是在单元测试中会存在问题,因为静态配置和映射在不同测试中是共享的,他们可能会互相影响。
我们希望使测试之间是独立的。为了达到这个目的,我们应该按如下规则设计我们的工程:
1. 总是使用IObjectMapper,不使用MapTo扩展方法。
2. 配置Abp.AutoMapper模块使用本地映射实例(使用单例模式注册到依赖注入)而不是静态实例(Abp.AutoMapper默认使用Mapper.Instance实例,这样就允许使用MapTo扩展方法)。
Configuration.Modules.AbpAutoMapper().UseStaticMapper = false;
Abp.AutoMapper模块定义了一个映射,用来转换本地字符串(或者ILocalizableString)对象到string对象。它使用ILocalizationManager进行转换,所以localizable属性在任何类的映射过程中会自动本地化。
可能需要直接使用AutoMapper的IMapper对象而不是IObjectMapper抽象。在这种情况下,在类中注入并使用IMapper。Abp.AutoMapper包以单例形式将IMapper注册到依赖注入。