写点什么

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

作者:何镇汐
  • 2023-11-11
    四川
  • 本文字数:8691 字

    阅读完需:约 29 分钟

对象到对象映射


Util 应用框架相似对象转换


本节介绍 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();
复制代码


用户头像

何镇汐

关注

15年以上.Net开发经验,擅长代码封装 2023-11-01 加入

15年以上.Net开发经验,擅长代码封装,主要作品为Util应用框架

评论

发布
暂无评论
Util应用框架基础(三)- 对象到对象映射_开源_何镇汐_InfoQ写作社区