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拥有更多的选项和能力来做对象之间的映射。详情请查询文档。
忽略字段
config.CreateMap<Order, OrderDto>()
.ForMember(u => u.PhoneNumber, options => options.Ignore());
字段名不一致
OrderDto增加手机号字段Tel,映射Order字段PhoneNumber
public class OrderDto
{
public string OrderName { get; set; }
public string Tel { get; set; }
}
config.CreateMap<Order, OrderDto>()
.ForMember(u => u.Tel, options => options.MapFrom(input => input.PhoneNumber));
需要对字段进行处理后返回
比如,隐藏11位手机号的中间4位
private static string HideTel(string input)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
var outReplace = Regex.Replace(input, "(\d{3})\d{4}(\d{4})", "$1****$2");
return outReplace;
}
config.CreateMap<Order, OrderDto>()
.ForMember(u => u.Tel, options => options.MapFrom(input => HideTel(input.PhoneNumber)));
拼接映射
又比如OrderDto新增邮寄地址和收货地址
namespace Demo.MyJob.Entity.Dto
{
public class OrderDto
{
public string OrderName { get; set; }
public string Tel { get; set; }
public string PostalAddress { get; set; }
public string DeliveryAddress { get; set; }
}
}
Order的相关表OrderAddress类型定义
namespace Demo.MyJob.Entity
{
public class OrderAddress
{
public string OrderId { get; set; }
public string PostalAddress { get; set; }
public string DeliveryAddress { get; set; }
}
}
这时就需要OrderAddress和Order的数据相结合映射OrderDto,怎么实现呢?借助元组Tuple。
config.CreateMap<(Order, OrderAddress), OrderDto>()
.ForMember(u => u.Tel, options => options.MapFrom(input => HideTel(input.Item1.PhoneNumber)))
.ForMember(u => u.OrderName, options => options.MapFrom(input => input.Item1.OrderName))
.ForMember(u => u.PostalAddress, options => options.MapFrom(input => input.Item2.PostalAddress))
.ForMember(u => u.DeliveryAddress, options => options.MapFrom(input => input.Item2.DeliveryAddress))
;
精简配置
需要自定义的映射关系过多时,会使得PreInitialize变大,不便于管理和查看。
public override void PreInitialize()
{
Configuration.Modules.AbpAutoMapper().Configurators.Add(config =>
{
config.CreateMap<(Order, OrderAddress), OrderDto>()
.ForMember(u => u.Tel, options => options.MapFrom(input => HideTel(input.Item1.PhoneNumber)))
.ForMember(u => u.OrderName, options => options.MapFrom(input => input.Item1.OrderName))
.ForMember(u => u.PostalAddress, options => options.MapFrom(input => input.Item2.PostalAddress))
.ForMember(u => u.DeliveryAddress, options => options.MapFrom(input => input.Item2.DeliveryAddress))
;
});
}
如何精简?新增类型MyMapperProfile,继承AutoMapper.Profile
using System.Text.RegularExpressions;
using AutoMapper;
using Demo.MyJob.Entity;
using Demo.MyJob.Entity.Dto;
namespace Demo.MyJob.MapperProfiles
{
class MyMapperProfile : Profile
{
private static string HideTel(string input)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
var outReplace = Regex.Replace(input, "(\d{3})\d{4}(\d{4})", "$1****$2");
return outReplace;
}
public MyMapperProfile()
{
CreateMap<Order, OrderDto>()
.ForMember(u => u.Tel, options => options.MapFrom(input => HideTel(input.PhoneNumber)));
CreateMap<(Order, OrderAddress), OrderDto>()
.ForMember(u => u.Tel, options => options.MapFrom(input => HideTel(input.Item1.PhoneNumber)))
.ForMember(u => u.OrderName, options => options.MapFrom(input => input.Item1.OrderName))
.ForMember(u => u.PostalAddress, options => options.MapFrom(input => input.Item2.PostalAddress))
.ForMember(u => u.DeliveryAddress, options => options.MapFrom(input => input.Item2.DeliveryAddress))
;
}
}
}
修改PreInitialize
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyJobCoreModule : AbpModule
{
public override void PreInitialize()
{
Configuration.Modules.AbpAutoMapper().Configurators.Add(config =>
{
config.AddMaps(typeof(MyJobCoreModule));
});
}
}
Abp.AutoMapper版本低于4.8.0的可以修改为
config.AddProfiles(typeof(MyJobCoreModule));
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的静态实例。对于应用程序代码这是简单且有效的,但是静态配置在单元测试的时候会有问题,因为在单元测试的时候,会在各个单元测试之间共享映射关系。