Util應(yīng)用框架基礎(chǔ)(二) - 對(duì)象到對(duì)象映射(AutoMapper)
本節(jié)介紹Util應(yīng)用框架相似對(duì)象之間的轉(zhuǎn)換方法.
文章分為多個(gè)小節(jié),如果對(duì)設(shè)計(jì)原理不感興趣,只需閱讀基礎(chǔ)用法部分即可.
概述
現(xiàn)代化分層架構(gòu),普遍采用了構(gòu)造塊DTO(數(shù)據(jù)傳輸對(duì)象).
DTO是一種參數(shù)對(duì)象,當(dāng)Web API接收到請(qǐng)求,請(qǐng)求參數(shù)被裝載到DTO對(duì)象中.
我們需要把 DTO 對(duì)象轉(zhuǎn)換成實(shí)體,才能保存到數(shù)據(jù)庫.
當(dāng)返回響應(yīng)消息時(shí),需要把實(shí)體轉(zhuǎn)換成DTO,再傳回客戶端.
對(duì)于簡單的系統(tǒng),DTO和實(shí)體非常相似,它們可能包含大量相同的屬性.
除此之外,還有很多場景也需要轉(zhuǎn)換相似對(duì)象.
下面的例子定義了學(xué)生實(shí)體和學(xué)生參數(shù)DTO.
它們包含兩個(gè)相同的屬性.
StudentService 是一個(gè)應(yīng)用服務(wù).
CreateAsync 方法創(chuàng)建學(xué)生,把DTO對(duì)象手工賦值轉(zhuǎn)換為學(xué)生實(shí)體,并添加到數(shù)據(jù)庫.
GetByIdAsync 方法通過ID獲取學(xué)生實(shí)體,并手工賦值轉(zhuǎn)換為學(xué)生DTO.
/// <summary>
/// 學(xué)生
/// </summary>
public class Student : AggregateRoot<Student> {
/// <summary>
/// 初始化學(xué)生
/// </summary>
public Student() : this( Guid.Empty ) {
}
/// <summary>
/// 初始化學(xué)生
/// </summary>
/// <param name="id">學(xué)生標(biāo)識(shí)</param>
public Student( Guid id ) : base( id ) {
}
/// <summary>
/// 姓名
///</summary>
public string Name { get; set; }
/// <summary>
/// 出生日期
///</summary>
public DateTime? Birthday { get; set; }
}
/// <summary>
/// 學(xué)生參數(shù)
/// </summary>
public class StudentDto : DtoBase {
/// <summary>
/// 姓名
///</summary>
public string Name { get; set; }
/// <summary>
/// 出生日期
///</summary>
public DateTime? Birthday { get; set; }
}
/// <summary>
/// 學(xué)生服務(wù)
/// </summary>
public class StudentService {
/// <summary>
/// 工作單元
/// </summary>
private IDemoUnitOfWork _demoUnitOfWork;
/// <summary>
/// 學(xué)生倉儲(chǔ)
/// </summary>
private IStudentRepository _repository;
/// <summary>
/// 初始化學(xué)生服務(wù)
/// </summary>
/// <param name="unitOfWork">工作單元</param>
/// <param name="repository">學(xué)生倉儲(chǔ)</param>
public StudentService( IDemoUnitOfWork unitOfWork, IStudentRepository repository ) {
_demoUnitOfWork = unitOfWork;
_repository = repository;
}
/// <summary>
/// 創(chuàng)建學(xué)生
/// </summary>
/// <param name="dto">學(xué)生參數(shù)</param>
public async Task CreateAsync( StudentDto dto ) {
var entity = new Student { Name = dto.Name, Birthday = dto.Birthday };
await _repository.AddAsync( entity );
await _demoUnitOfWork.CommitAsync();
}
/// <summary>
/// 獲取學(xué)生
/// </summary>
/// <param name="id">學(xué)生標(biāo)識(shí)</param>
public async Task<StudentDto> GetByIdAsync( Guid id ) {
var entity = await _repository.FindByIdAsync( id );
return new StudentDto { Name = entity.Name, Birthday = entity.Birthday };
}
}
學(xué)生范例只有兩個(gè)屬性,手工轉(zhuǎn)換工作量并不大.
但真實(shí)的應(yīng)用每個(gè)對(duì)象可能包含數(shù)十個(gè)屬性,使用手工賦值的方式轉(zhuǎn)換,效率低下且容易出錯(cuò).
我們需要一種自動(dòng)化的轉(zhuǎn)換手段.
對(duì)象到對(duì)象映射框架 AutoMapper
Util應(yīng)用框架使用 AutoMapper ,它是 .Net 最流行的對(duì)象間映射框架.
AutoMapper 可以自動(dòng)轉(zhuǎn)換相同名稱和類型的屬性,同時(shí)支持一些約定轉(zhuǎn)換方式.
基礎(chǔ)用法
引用Nuget包
Nuget包名: Util.ObjectMapping.AutoMapper.
通常不需要手工引用它.
MapTo 擴(kuò)展方法
Util應(yīng)用框架在根對(duì)象 object 擴(kuò)展了 MapTo 方法,你可以在任何對(duì)象上調(diào)用 MapTo 進(jìn)行對(duì)象轉(zhuǎn)換.
擴(kuò)展方法需要引用命名空間, MapTo 擴(kuò)展方法在 Util 命名空間.
using Util;
有兩種調(diào)用形式.
-
調(diào)用形式1: 源對(duì)象實(shí)例.MapTo<目標(biāo)類型>()
- 范例: 這里的源對(duì)象實(shí)例是學(xué)生參數(shù) dto,目標(biāo)類型是 Student,返回 Student 對(duì)象實(shí)例.
/// <summary> /// 創(chuàng)建學(xué)生 /// </summary> /// <param name="dto">學(xué)生參數(shù)</param> public async Task CreateAsync( StudentDto dto ) { var entity = dto.MapTo<Student>(); ... } -
調(diào)用形式2: 源對(duì)象實(shí)例.MapTo(目標(biāo)類型實(shí)例)
當(dāng)目標(biāo)類型實(shí)例已經(jīng)存在時(shí)使用該重載.
- 范例:
/// <summary> /// 創(chuàng)建學(xué)生 /// </summary> /// <param name="dto">學(xué)生參數(shù)</param> public async Task CreateAsync( StudentDto dto ) { var entity = new Student(); dto.MapTo(entity); ... }
MapToList 擴(kuò)展方法
Util應(yīng)用框架在 IEnumerable 擴(kuò)展了 MapToList 方法.
如果要轉(zhuǎn)換集合,使用該擴(kuò)展.
范例
將 StudentDto 集合轉(zhuǎn)換為 Student 集合.
傳入泛型參數(shù) Student ,而不是 List<Student> .
List<StudentDto> dtos = new List<StudentDto> { new() { Name = "a" }, new() { Name = "b" } };
List<Student> entities = dtos.MapToList<Student>();
配置 AutoMapper
對(duì)于簡單場景,比如轉(zhuǎn)換對(duì)象的屬性都相同, 不需要任何配置.
AutoMapper服務(wù)注冊(cè)器自動(dòng)完成基礎(chǔ)配置.
不過很多業(yè)務(wù)場景轉(zhuǎn)換的對(duì)象具有差異,需要配置差異部分.
Util.ObjectMapping.IAutoMapperConfig
Util提供了 AutoMapper 配置接口 IAutoMapperConfig.
/// <summary>
/// AutoMapper配置
/// </summary>
public interface IAutoMapperConfig {
/// <summary>
/// 配置映射
/// </summary>
/// <param name="expression">配置映射表達(dá)式</param>
void Config( IMapperConfigurationExpression expression );
}
Config 配置方法提供配置映射表達(dá)式 IMapperConfigurationExpression 實(shí)例,它是 AutoMapper 配置入口.
由 AutoMapper 服務(wù)注冊(cè)器掃描執(zhí)行所有 IAutoMapperConfig 配置.
約定: 將 AutoMapper 配置類放置在 ObjectMapping 目錄中.
為每一對(duì)有差異的對(duì)象實(shí)現(xiàn)該接口.
修改學(xué)生示例,把 StudentDto 的 Name 屬性名改為 FullName.
由于學(xué)生實(shí)體和DTO的Name屬性名不同,所以不能自動(dòng)轉(zhuǎn)換,需要配置.
需要配置兩個(gè)映射方向.
-
從 Student 到 StudentDto.
-
從 StudentDto 到 Student.
/// <summary>
/// 學(xué)生
/// </summary>
public class Student : AggregateRoot<Student> {
/// <summary>
/// 初始化學(xué)生
/// </summary>
public Student() : this( Guid.Empty ) {
}
/// <summary>
/// 初始化學(xué)生
/// </summary>
/// <param name="id">學(xué)生標(biāo)識(shí)</param>
public Student( Guid id ) : base( id ) {
}
/// <summary>
/// 姓名
///</summary>
public string Name { get; set; }
/// <summary>
/// 出生日期
///</summary>
public DateTime? Birthday { get; set; }
}
/// <summary>
/// 學(xué)生參數(shù)
/// </summary>
public class StudentDto : DtoBase {
/// <summary>
/// 姓名
///</summary>
public string FullName { get; set; }
/// <summary>
/// 出生日期
///</summary>
public DateTime? Birthday { get; set; }
}
/// <summary>
/// 學(xué)生映射配置
/// </summary>
public class StudentAutoMapperConfig : IAutoMapperConfig {
/// <summary>
/// 配置映射
/// </summary>
/// <param name="expression">配置映射表達(dá)式</param>
public void Config( IMapperConfigurationExpression expression ) {
expression.CreateMap<Student, StudentDto>()
.ForMember( t => t.FullName, t => t.MapFrom( r => r.Name ) );
expression.CreateMap<StudentDto,Student>()
.ForMember( t => t.Name, t => t.MapFrom( r => r.FullName ) );
}
}
對(duì)象間映射最佳實(shí)踐
應(yīng)該盡量避免配置,保持代碼簡單.
-
統(tǒng)一對(duì)象屬性
如果有可能,盡量統(tǒng)一對(duì)象屬性名稱和屬性類型.
-
使用 AutoMapper 映射約定
AutoMapper 支持一些約定的映射方式.
范例
添加班級(jí)類型,學(xué)生實(shí)體添加班級(jí)關(guān)聯(lián)實(shí)體 Class, 學(xué)生DTO添加班級(jí)名稱屬性 ClassName.
/// <summary> /// 學(xué)生 /// </summary> public class Student : AggregateRoot<Student> { /// <summary> /// 初始化學(xué)生 /// </summary> public Student() : this( Guid.Empty ) { } /// <summary> /// 初始化學(xué)生 /// </summary> /// <param name="id">學(xué)生標(biāo)識(shí)</param> public Student( Guid id ) : base( id ) { Class = new Class(); } /// <summary> /// 姓名 ///</summary> public string Name { get; set; } /// <summary> /// 出生日期 ///</summary> public DateTime? Birthday { get; set; } /// <summary> /// 班級(jí) /// </summary> public Class Class { get; set; } } /// <summary> /// 班級(jí) /// </summary> public class Class : AggregateRoot<Class> { /// <summary> /// 初始化班級(jí) /// </summary> public Class() : this( Guid.Empty ) { } /// <summary> /// 初始化班級(jí) /// </summary> /// <param name="id">班級(jí)標(biāo)識(shí)</param> public Class( Guid id ) : base( id ) { } /// <summary> /// 班級(jí)名稱 ///</summary> public string Name { get; set; } } /// <summary> /// 學(xué)生參數(shù) /// </summary> public class StudentDto : DtoBase { /// <summary> /// 姓名 ///</summary> public string Name { get; set; } /// <summary> /// 班級(jí)名稱 ///</summary> public string ClassName { get; set; } /// <summary> /// 出生日期 ///</summary> public DateTime? Birthday { get; set; } }將 Student 的 Class實(shí)體 Name 屬性映射到 StudentDto 的 ClassName 屬性 ,不需要配置.
var entity = new Student { Class = new Class { Name = "a" } }; var dto = entity.MapTo<StudentDto>(); //dto.ClassName 值為 a但不支持從 StudentDto 的 ClassName 屬性映射到 Student 的 Class實(shí)體 Name 屬性.
var dto = new StudentDto { ClassName = "a" }; var entity = dto.MapTo<Student>(); //entity.Class.Name 值為 null
源碼解析
對(duì)象映射器 IObjectMapper
你不需要調(diào)用 IObjectMapper 接口,始終通過 MapTo 擴(kuò)展方法進(jìn)行轉(zhuǎn)換.
ObjectMapper 實(shí)現(xiàn)了 IObjectMapper 接口.
ObjectMapper映射源類型和目標(biāo)類型時(shí),如果發(fā)現(xiàn)尚未配置映射關(guān)系,則自動(dòng)配置.
除了自動(dòng)配置映射關(guān)系外,還需要處理并發(fā)和異常情況.
/// <summary>
/// 對(duì)象映射器
/// </summary>
public interface IObjectMapper {
/// <summary>
/// 將源對(duì)象映射到目標(biāo)對(duì)象
/// </summary>
/// <typeparam name="TSource">源類型</typeparam>
/// <typeparam name="TDestination">目標(biāo)類型</typeparam>
/// <param name="source">源對(duì)象</param>
TDestination Map<TSource, TDestination>( TSource source );
/// <summary>
/// 將源對(duì)象映射到目標(biāo)對(duì)象
/// </summary>
/// <typeparam name="TSource">源類型</typeparam>
/// <typeparam name="TDestination">目標(biāo)類型</typeparam>
/// <param name="source">源對(duì)象</param>
/// <param name="destination">目標(biāo)對(duì)象</param>
TDestination Map<TSource, TDestination>( TSource source, TDestination destination );
}
/// <summary>
/// AutoMapper對(duì)象映射器
/// </summary>
public class ObjectMapper : IObjectMapper {
/// <summary>
/// 最大遞歸獲取結(jié)果次數(shù)
/// </summary>
private const int MaxGetResultCount = 16;
/// <summary>
/// 同步鎖
/// </summary>
private static readonly object Sync = new();
/// <summary>
/// 配置表達(dá)式
/// </summary>
private readonly MapperConfigurationExpression _configExpression;
/// <summary>
/// 配置提供器
/// </summary>
private IConfigurationProvider _config;
/// <summary>
/// 對(duì)象映射器
/// </summary>
private IMapper _mapper;
/// <summary>
/// 初始化AutoMapper對(duì)象映射器
/// </summary>
/// <param name="expression">配置表達(dá)式</param>
public ObjectMapper( MapperConfigurationExpression expression ) {
_configExpression = expression ?? throw new ArgumentNullException( nameof( expression ) );
_config = new MapperConfiguration( expression );
_mapper = _config.CreateMapper();
}
/// <summary>
/// 將源對(duì)象映射到目標(biāo)對(duì)象
/// </summary>
/// <typeparam name="TSource">源類型</typeparam>
/// <typeparam name="TDestination">目標(biāo)類型</typeparam>
/// <param name="source">源對(duì)象</param>
public TDestination Map<TSource, TDestination>( TSource source ) {
return Map<TSource, TDestination>( source, default );
}
/// <summary>
/// 將源對(duì)象映射到目標(biāo)對(duì)象
/// </summary>
/// <typeparam name="TSource">源類型</typeparam>
/// <typeparam name="TDestination">目標(biāo)類型</typeparam>
/// <param name="source">源對(duì)象</param>
/// <param name="destination">目標(biāo)對(duì)象</param>
public TDestination Map<TSource, TDestination>( TSource source, TDestination destination ) {
if ( source == null )
return default;
var sourceType = GetType( source );
var destinationType = GetType( destination );
return GetResult( sourceType, destinationType, source, destination,0 );
}
/// <summary>
/// 獲取類型
/// </summary>
private Type GetType<T>( T obj ) {
if( obj == null )
return GetType( typeof( T ) );
return GetType( obj.GetType() );
}
/// <summary>
/// 獲取類型
/// </summary>
private Type GetType( Type type ) {
return Reflection.GetElementType( type );
}
/// <summary>
/// 獲取結(jié)果
/// </summary>
private TDestination GetResult<TDestination>( Type sourceType, Type destinationType, object source, TDestination destination,int i ) {
try {
if ( i >= MaxGetResultCount )
return default;
i += 1;
if ( Exists( sourceType, destinationType ) )
return GetResult( source, destination );
lock ( Sync ) {
if ( Exists( sourceType, destinationType ) )
return GetResult( source, destination );
ConfigMap( sourceType, destinationType );
}
return GetResult( source, destination );
}
catch ( AutoMapperMappingException ex ) {
if ( ex.InnerException != null && ex.InnerException.Message.StartsWith( "Missing type map configuration" ) )
return GetResult( GetType( ex.MemberMap.SourceType ), GetType( ex.MemberMap.DestinationType ), source, destination,i );
throw;
}
}
/// <summary>
/// 是否已存在映射配置
/// </summary>
private bool Exists( Type sourceType, Type destinationType ) {
return _config.Internal().FindTypeMapFor( sourceType, destinationType ) != null;
}
/// <summary>
/// 獲取映射結(jié)果
/// </summary>
private TDestination GetResult<TSource, TDestination>( TSource source, TDestination destination ) {
return _mapper.Map( source, destination );
}
/// <summary>
/// 動(dòng)態(tài)配置映射
/// </summary>
private void ConfigMap( Type sourceType, Type destinationType ) {
_configExpression.CreateMap( sourceType, destinationType );
_config = new MapperConfiguration( _configExpression );
_mapper = _config.CreateMapper();
}
}
AutoMapper服務(wù)注冊(cè)器
AutoMapper服務(wù)注冊(cè)器掃描 IAutoMapperConfig 配置并執(zhí)行.
同時(shí)為 MapTo 擴(kuò)展類 ObjectMapperExtensions 設(shè)置 IObjectMapper 實(shí)例.
/// <summary>
/// AutoMapper服務(wù)注冊(cè)器
/// </summary>
public class AutoMapperServiceRegistrar : IServiceRegistrar {
/// <summary>
/// 獲取服務(wù)名
/// </summary>
public static string ServiceName => "Util.ObjectMapping.Infrastructure.AutoMapperServiceRegistrar";
/// <summary>
/// 排序號(hào)
/// </summary>
public int OrderId => 300;
/// <summary>
/// 是否啟用
/// </summary>
public bool Enabled => ServiceRegistrarConfig.IsEnabled( ServiceName );
/// <summary>
/// 注冊(cè)服務(wù)
/// </summary>
/// <param name="serviceContext">服務(wù)上下文</param>
public Action Register( ServiceContext serviceContext ) {
var types = serviceContext.TypeFinder.Find<IAutoMapperConfig>();
var instances = types.Select( type => Reflection.CreateInstance<IAutoMapperConfig>( type ) ).ToList();
var expression = new MapperConfigurationExpression();
instances.ForEach( t => t.Config( expression ) );
var mapper = new ObjectMapper( expression );
ObjectMapperExtensions.SetMapper( mapper );
serviceContext.HostBuilder.ConfigureServices( ( context, services ) => {
services.AddSingleton<IObjectMapper>( mapper );
} );
return null;
}
}
對(duì)象映射擴(kuò)展 ObjectMapperExtensions
ObjectMapperExtensions 提供了 MapTo 和 MapToList 擴(kuò)展方法.
MapTo 擴(kuò)展方法依賴 IObjectMapper 實(shí)例,由于擴(kuò)展方法是靜態(tài)方法,所以需要將 IObjectMapper 定義為靜態(tài)變量.
通過 SetMapper 靜態(tài)方法將對(duì)象映射器實(shí)例傳入.
對(duì)象映射器 ObjectMapper 實(shí)例作為靜態(tài)變量,必須處理并發(fā)相關(guān)的問題.
/// <summary>
/// 對(duì)象映射擴(kuò)展
/// </summary>
public static class ObjectMapperExtensions {
/// <summary>
/// 對(duì)象映射器
/// </summary>
private static IObjectMapper _mapper;
/// <summary>
/// 設(shè)置對(duì)象映射器
/// </summary>
/// <param name="mapper">對(duì)象映射器</param>
public static void SetMapper( IObjectMapper mapper ) {
_mapper = mapper ?? throw new ArgumentNullException( nameof( mapper ) );
}
/// <summary>
/// 將源對(duì)象映射到目標(biāo)對(duì)象
/// </summary>
/// <typeparam name="TDestination">目標(biāo)類型</typeparam>
/// <param name="source">源對(duì)象</param>
public static TDestination MapTo<TDestination>( this object source ) {
if ( _mapper == null )
throw new ArgumentNullException( nameof(_mapper) );
return _mapper.Map<object, TDestination>( source );
}
/// <summary>
/// 將源對(duì)象映射到目標(biāo)對(duì)象
/// </summary>
/// <typeparam name="TSource">源類型</typeparam>
/// <typeparam name="TDestination">目標(biāo)類型</typeparam>
/// <param name="source">源對(duì)象</param>
/// <param name="destination">目標(biāo)對(duì)象</param>
public static TDestination MapTo<TSource, TDestination>( this TSource source, TDestination destination ) {
if( _mapper == null )
throw new ArgumentNullException( nameof( _mapper ) );
return _mapper.Map( source, destination );
}
/// <summary>
/// 將源集合映射到目標(biāo)集合
/// </summary>
/// <typeparam name="TDestination">目標(biāo)元素類型,范例:Sample,不要加List</typeparam>
/// <param name="source">源集合</param>
public static List<TDestination> MapToList<TDestination>( this System.Collections.IEnumerable source ) {
return MapTo<List<TDestination>>( source );
}
}
禁用 AutoMapper 服務(wù)注冊(cè)器
ServiceRegistrarConfig.Instance.DisableAutoMapperServiceRegistrar();

浙公網(wǎng)安備 33010602011771號(hào)