Util应用框架基础(二) - 对象到对象映射(AutoMapper)
本節介紹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();
總結
以上是生活随笔為你收集整理的Util应用框架基础(二) - 对象到对象映射(AutoMapper)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《田耕纪》大结局:很多人不理解,为什么连
- 下一篇: 持续进化,快速转录,Faster-Whi