本節介紹Util應用框架相似物件之間的轉換方法.
文章分為多個小節,如果對設計原理不感興趣,只需閱讀基礎用法部分即可.
概述
現代化分層架構,普遍採用了構造塊DTO(資料傳輸物件).
DTO是一種引數物件,當Web API接收到請求,請求引數被裝載到DTO物件中.
我們需要把 DTO 物件轉換成實體,才能儲存到資料庫.
當返回響應訊息時,需要把實體轉換成DTO,再傳回客戶端.
對於簡單的系統,DTO和實體非常相似,它們可能包含大量相同的屬性.
除此之外,還有很多場景也需要轉換相似物件.
下面的例子定義了學生實體和學生引數DTO.
它們包含兩個相同的屬性.
StudentService 是一個應用服務.
CreateAsync 方法建立學生,把DTO物件手工賦值轉換為學生實體,並新增到資料庫.
GetByIdAsync 方法透過ID獲取學生實體,並手工賦值轉換為學生DTO.
/// <summary>
/// 學生
/// </summary>
public class Student : AggregateRoot<Student> {
/// <summary>
/// 初始化學生
/// </summary>
public Student() : this( Guid.Empty ) {
}
/// <summary>
/// 初始化學生
/// </summary>
/// <param name="id">學生標識</param>
public Student( Guid id ) : base( id ) {
}
/// <summary>
/// 姓名
///</summary>
public string Name { get; set; }
/// <summary>
/// 出生日期
///</summary>
public DateTime? Birthday { get; set; }
}
/// <summary>
/// 學生引數
/// </summary>
public class StudentDto : DtoBase {
/// <summary>
/// 姓名
///</summary>
public string Name { get; set; }
/// <summary>
/// 出生日期
///</summary>
public DateTime? Birthday { get; set; }
}
/// <summary>
/// 學生服務
/// </summary>
public class StudentService {
/// <summary>
/// 工作單元
/// </summary>
private IDemoUnitOfWork _demoUnitOfWork;
/// <summary>
/// 學生倉儲
/// </summary>
private IStudentRepository _repository;
/// <summary>
/// 初始化學生服務
/// </summary>
/// <param name="unitOfWork">工作單元</param>
/// <param name="repository">學生倉儲</param>
public StudentService( IDemoUnitOfWork unitOfWork, IStudentRepository repository ) {
_demoUnitOfWork = unitOfWork;
_repository = repository;
}
/// <summary>
/// 建立學生
/// </summary>
/// <param name="dto">學生引數</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>
/// 獲取學生
/// </summary>
/// <param name="id">學生標識</param>
public async Task<StudentDto> GetByIdAsync( Guid id ) {
var entity = await _repository.FindByIdAsync( id );
return new StudentDto { Name = entity.Name, Birthday = entity.Birthday };
}
}
學生範例只有兩個屬性,手工轉換工作量並不大.
但真實的應用每個物件可能包含數十個屬性,使用手工賦值的方式轉換,效率低下且容易出錯.
我們需要一種自動化的轉換手段.
物件到物件對映框架 AutoMapper
Util應用框架使用 AutoMapper ,它是 .Net 最流行的物件間對映框架.
AutoMapper 可以自動轉換相同名稱和型別的屬性,同時支援一些約定轉換方式.
基礎用法
引用Nuget包
Nuget包名: Util.ObjectMapping.AutoMapper.
通常不需要手工引用它.
MapTo 擴充套件方法
Util應用框架在根物件 object 擴充套件了 MapTo 方法,你可以在任何物件上呼叫 MapTo 進行物件轉換.
擴充套件方法需要引用名稱空間, MapTo 擴充套件方法在 Util 名稱空間.
using Util;
有兩種呼叫形式.
-
呼叫形式1: 源物件例項.MapTo<目標型別>()
- 範例: 這裡的源物件例項是學生引數 dto,目標型別是 Student,返回 Student 物件例項.
/// <summary> /// 建立學生 /// </summary> /// <param name="dto">學生引數</param> public async Task CreateAsync( StudentDto dto ) { var entity = dto.MapTo<Student>(); ... }
-
呼叫形式2: 源物件例項.MapTo(目標型別例項)
當目標型別例項已經存在時使用該過載.
- 範例:
/// <summary> /// 建立學生 /// </summary> /// <param name="dto">學生引數</param> public async Task CreateAsync( StudentDto dto ) { var entity = new Student(); dto.MapTo(entity); ... }
MapToList 擴充套件方法
Util應用框架在 IEnumerable 擴充套件了 MapToList 方法.
如果要轉換集合,使用該擴充套件.
範例
將 StudentDto 集合轉換為 Student 集合.
傳入泛型引數 Student ,而不是 List<Student> .
List<StudentDto> dtos = new List<StudentDto> { new() { Name = "a" }, new() { Name = "b" } };
List<Student> entities = dtos.MapToList<Student>();
配置 AutoMapper
對於簡單場景,比如轉換物件的屬性都相同, 不需要任何配置.
AutoMapper服務註冊器自動完成基礎配置.
不過很多業務場景轉換的物件具有差異,需要配置差異部分.
Util.ObjectMapping.IAutoMapperConfig
Util提供了 AutoMapper 配置介面 IAutoMapperConfig.
/// <summary>
/// AutoMapper配置
/// </summary>
public interface IAutoMapperConfig {
/// <summary>
/// 配置對映
/// </summary>
/// <param name="expression">配置對映表示式</param>
void Config( IMapperConfigurationExpression expression );
}
Config 配置方法提供配置對映表示式 IMapperConfigurationExpression 例項,它是 AutoMapper 配置入口.
由 AutoMapper 服務註冊器掃描執行所有 IAutoMapperConfig 配置.
約定: 將 AutoMapper 配置類放置在 ObjectMapping 目錄中.
為每一對有差異的物件實現該介面.
修改學生示例,把 StudentDto 的 Name 屬性名改為 FullName.
由於學生實體和DTO的Name屬性名不同,所以不能自動轉換,需要配置.
需要配置兩個對映方向.
-
從 Student 到 StudentDto.
-
從 StudentDto 到 Student.
/// <summary>
/// 學生
/// </summary>
public class Student : AggregateRoot<Student> {
/// <summary>
/// 初始化學生
/// </summary>
public Student() : this( Guid.Empty ) {
}
/// <summary>
/// 初始化學生
/// </summary>
/// <param name="id">學生標識</param>
public Student( Guid id ) : base( id ) {
}
/// <summary>
/// 姓名
///</summary>
public string Name { get; set; }
/// <summary>
/// 出生日期
///</summary>
public DateTime? Birthday { get; set; }
}
/// <summary>
/// 學生引數
/// </summary>
public class StudentDto : DtoBase {
/// <summary>
/// 姓名
///</summary>
public string FullName { get; set; }
/// <summary>
/// 出生日期
///</summary>
public DateTime? Birthday { get; set; }
}
/// <summary>
/// 學生對映配置
/// </summary>
public class StudentAutoMapperConfig : IAutoMapperConfig {
/// <summary>
/// 配置對映
/// </summary>
/// <param name="expression">配置對映表示式</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 ) );
}
}
物件間對映最佳實踐
應該儘量避免配置,保持程式碼簡單.
-
統一物件屬性
如果有可能,儘量統一物件屬性名稱和屬性型別.
-
使用 AutoMapper 對映約定
AutoMapper 支援一些約定的對映方式.
範例
新增班級型別,學生實體新增班級關聯實體 Class, 學生DTO新增班級名稱屬性 ClassName.
/// <summary> /// 學生 /// </summary> public class Student : AggregateRoot<Student> { /// <summary> /// 初始化學生 /// </summary> public Student() : this( Guid.Empty ) { } /// <summary> /// 初始化學生 /// </summary> /// <param name="id">學生標識</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> /// 班級 /// </summary> public Class Class { get; set; } } /// <summary> /// 班級 /// </summary> public class Class : AggregateRoot<Class> { /// <summary> /// 初始化班級 /// </summary> public Class() : this( Guid.Empty ) { } /// <summary> /// 初始化班級 /// </summary> /// <param name="id">班級標識</param> public Class( Guid id ) : base( id ) { } /// <summary> /// 班級名稱 ///</summary> public string Name { get; set; } } /// <summary> /// 學生引數 /// </summary> public class StudentDto : DtoBase { /// <summary> /// 姓名 ///</summary> public string Name { get; set; } /// <summary> /// 班級名稱 ///</summary> public string ClassName { get; set; } /// <summary> /// 出生日期 ///</summary> public DateTime? Birthday { get; set; } }
將 Student 的 Class實體 Name 屬性對映到 StudentDto 的 ClassName 屬性 ,不需要配置.
var entity = new Student { Class = new Class { Name = "a" } }; var dto = entity.MapTo<StudentDto>(); //dto.ClassName 值為 a
但不支援從 StudentDto 的 ClassName 屬性對映到 Student 的 Class實體 Name 屬性.
var dto = new StudentDto { ClassName = "a" }; var entity = dto.MapTo<Student>(); //entity.Class.Name 值為 null
原始碼解析
物件對映器 IObjectMapper
你不需要呼叫 IObjectMapper 介面,始終透過 MapTo 擴充套件方法進行轉換.
ObjectMapper 實現了 IObjectMapper 介面.
ObjectMapper對映源型別和目標型別時,如果發現尚未配置對映關係,則自動配置.
除了自動配置對映關係外,還需要處理併發和異常情況.
/// <summary>
/// 物件對映器
/// </summary>
public interface IObjectMapper {
/// <summary>
/// 將源物件對映到目標物件
/// </summary>
/// <typeparam name="TSource">源型別</typeparam>
/// <typeparam name="TDestination">目標型別</typeparam>
/// <param name="source">源物件</param>
TDestination Map<TSource, TDestination>( TSource source );
/// <summary>
/// 將源物件對映到目標物件
/// </summary>
/// <typeparam name="TSource">源型別</typeparam>
/// <typeparam name="TDestination">目標型別</typeparam>
/// <param name="source">源物件</param>
/// <param name="destination">目標物件</param>
TDestination Map<TSource, TDestination>( TSource source, TDestination destination );
}
/// <summary>
/// AutoMapper物件對映器
/// </summary>
public class ObjectMapper : IObjectMapper {
/// <summary>
/// 最大遞迴獲取結果次數
/// </summary>
private const int MaxGetResultCount = 16;
/// <summary>
/// 同步鎖
/// </summary>
private static readonly object Sync = new();
/// <summary>
/// 配置表示式
/// </summary>
private readonly MapperConfigurationExpression _configExpression;
/// <summary>
/// 配置提供器
/// </summary>
private IConfigurationProvider _config;
/// <summary>
/// 物件對映器
/// </summary>
private IMapper _mapper;
/// <summary>
/// 初始化AutoMapper物件對映器
/// </summary>
/// <param name="expression">配置表示式</param>
public ObjectMapper( MapperConfigurationExpression expression ) {
_configExpression = expression ?? throw new ArgumentNullException( nameof( expression ) );
_config = new MapperConfiguration( expression );
_mapper = _config.CreateMapper();
}
/// <summary>
/// 將源物件對映到目標物件
/// </summary>
/// <typeparam name="TSource">源型別</typeparam>
/// <typeparam name="TDestination">目標型別</typeparam>
/// <param name="source">源物件</param>
public TDestination Map<TSource, TDestination>( TSource source ) {
return Map<TSource, TDestination>( source, default );
}
/// <summary>
/// 將源物件對映到目標物件
/// </summary>
/// <typeparam name="TSource">源型別</typeparam>
/// <typeparam name="TDestination">目標型別</typeparam>
/// <param name="source">源物件</param>
/// <param name="destination">目標物件</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>
/// 獲取結果
/// </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>
/// 獲取對映結果
/// </summary>
private TDestination GetResult<TSource, TDestination>( TSource source, TDestination destination ) {
return _mapper.Map( source, destination );
}
/// <summary>
/// 動態配置對映
/// </summary>
private void ConfigMap( Type sourceType, Type destinationType ) {
_configExpression.CreateMap( sourceType, destinationType );
_config = new MapperConfiguration( _configExpression );
_mapper = _config.CreateMapper();
}
}
AutoMapper服務註冊器
AutoMapper服務註冊器掃描 IAutoMapperConfig 配置並執行.
同時為 MapTo 擴充套件類 ObjectMapperExtensions 設定 IObjectMapper 例項.
/// <summary>
/// AutoMapper服務註冊器
/// </summary>
public class AutoMapperServiceRegistrar : IServiceRegistrar {
/// <summary>
/// 獲取服務名
/// </summary>
public static string ServiceName => "Util.ObjectMapping.Infrastructure.AutoMapperServiceRegistrar";
/// <summary>
/// 排序號
/// </summary>
public int OrderId => 300;
/// <summary>
/// 是否啟用
/// </summary>
public bool Enabled => ServiceRegistrarConfig.IsEnabled( ServiceName );
/// <summary>
/// 註冊服務
/// </summary>
/// <param name="serviceContext">服務上下文</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;
}
}
物件對映擴充套件 ObjectMapperExtensions
ObjectMapperExtensions 提供了 MapTo 和 MapToList 擴充套件方法.
MapTo 擴充套件方法依賴 IObjectMapper 例項,由於擴充套件方法是靜態方法,所以需要將 IObjectMapper 定義為靜態變數.
透過 SetMapper 靜態方法將物件對映器例項傳入.
物件對映器 ObjectMapper 例項作為靜態變數,必須處理併發相關的問題.
/// <summary>
/// 物件對映擴充套件
/// </summary>
public static class ObjectMapperExtensions {
/// <summary>
/// 物件對映器
/// </summary>
private static IObjectMapper _mapper;
/// <summary>
/// 設定物件對映器
/// </summary>
/// <param name="mapper">物件對映器</param>
public static void SetMapper( IObjectMapper mapper ) {
_mapper = mapper ?? throw new ArgumentNullException( nameof( mapper ) );
}
/// <summary>
/// 將源物件對映到目標物件
/// </summary>
/// <typeparam name="TDestination">目標型別</typeparam>
/// <param name="source">源物件</param>
public static TDestination MapTo<TDestination>( this object source ) {
if ( _mapper == null )
throw new ArgumentNullException( nameof(_mapper) );
return _mapper.Map<object, TDestination>( source );
}
/// <summary>
/// 將源物件對映到目標物件
/// </summary>
/// <typeparam name="TSource">源型別</typeparam>
/// <typeparam name="TDestination">目標型別</typeparam>
/// <param name="source">源物件</param>
/// <param name="destination">目標物件</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>
/// 將源集合對映到目標集合
/// </summary>
/// <typeparam name="TDestination">目標元素型別,範例:Sample,不要加List</typeparam>
/// <param name="source">源集合</param>
public static List<TDestination> MapToList<TDestination>( this System.Collections.IEnumerable source ) {
return MapTo<List<TDestination>>( source );
}
}
禁用 AutoMapper 服務註冊器
ServiceRegistrarConfig.Instance.DisableAutoMapperServiceRegistrar();