Util应用框架基础(二) - 对象到对象映射(AutoMapper)

本节介绍Util应用框架相似对象之间的转换方法.

文章分为多个小节,如果对设计原理不感兴趣,只需阅读基础用法部分即可.

概述

现代化分层架构,普遍采用了构造块DTO(数据传输对象).

DTO是一种参数对象,当Web API接收到请求,请求参数被装载到DTO对象中.

我们需要把 DTO 对象转换成实体,才能保存到数据库.

当返回响应消息时,需要把实体转换成DTO,再传回客户端.

对于简单的系统,DTO和实体非常相似,它们可能包含大量相同的属性.

除此之外,还有很多场景也需要转换相似对象.

下面的例子定义了学生实体和学生参数DTO.

它们包含两个相同的属性.

StudentService 是一个应用服务.

CreateAsync 方法创建学生,把DTO对象手工赋值转换为学生实体,并添加到数据库.

GetByIdAsync 方法通过ID获取学生实体,并手工赋值转换为学生DTO.

c# 复制代码
/// <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 对象实例.
    c# 复制代码
      /// <summary>
      /// 创建学生
      /// </summary>
      /// <param name="dto">学生参数</param>
      public async Task CreateAsync( StudentDto dto ) {
          var entity = dto.MapTo<Student>();
          ...
      }
  • 调用形式2: 源对象实例.MapTo(目标类型实例)

    当目标类型实例已经存在时使用该重载.

    • 范例:
    c# 复制代码
      /// <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> .

c# 复制代码
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.

c# 复制代码
/// <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.

c# 复制代码
/// <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.

    c# 复制代码
      /// <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 属性 ,不需要配置.

    c# 复制代码
    var entity = new Student { Class = new Class { Name = "a" } };
    var dto = entity.MapTo<StudentDto>();
    //dto.ClassName 值为 a

    但不支持从 StudentDto 的 ClassName 属性映射到 Student 的 Class实体 Name 属性.

    c# 复制代码
    var dto = new StudentDto { ClassName = "a" };
    var entity = dto.MapTo<Student>();
    //entity.Class.Name 值为 null

源码解析

对象映射器 IObjectMapper

你不需要调用 IObjectMapper 接口,始终通过 MapTo 扩展方法进行转换.

ObjectMapper 实现了 IObjectMapper 接口.

ObjectMapper映射源类型和目标类型时,如果发现尚未配置映射关系,则自动配置.

除了自动配置映射关系外,还需要处理并发和异常情况.

c# 复制代码
/// <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 实例.

c# 复制代码
/// <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 实例作为静态变量,必须处理并发相关的问题.

c# 复制代码
/// <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 服务注册器

c# 复制代码
ServiceRegistrarConfig.Instance.DisableAutoMapperServiceRegistrar();