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的靜態實例。對於應用程序代碼這是簡單且有效的,但是靜態配置在單元測試的時候會有問題,因為在單元測試的時候,會在各個單元測試之間共享映射關系。
