Util應用框架基礎(二) - 物件到物件對映(AutoMapper)

何鎮汐發表於2023-11-03

本節介紹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 提供了 MapToMapToList 擴充套件方法.

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();

相關文章