本节介绍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();

Util应用框架基础(二) - 对象到对象映射(AutoMapper)的更多相关文章

  1. Bootstrap<基础二十七> 多媒体对象(Media Object)

    Bootstrap 中的多媒体对象(Media Object).这些抽象的对象样式用于创建各种类型的组件(比如:博客评论),我们可以在组件中使用图文混排,图像可以左对齐或者右对齐.媒体对象可以用更少的 ...

  2. Django框架基础知识08-表关联对象及多表查询

    1.自定义主键字段的创建 AutoFiled(pirmary_key=True) # 一般不会自定义,int类型,自增长 一般不自定义主键. 2.order_by asc desc from djan ...

  3. Hibernate框架(二)POJO对象的操作

    POJO对象其实就是我们的实体,这篇博客总结一下框架对POJO对象对应数据库主键的生成策略,和一些对POJO对象的简单增删改查的操作. 一,Hibernate框架中主键的生成策略有三种方式: 1,数据 ...

  4. JavaScript 基础(二) - 创建 function 对象的方法, String对象, Array对象

    创建 function 对象的两种方法: 方式一(推荐) function func1(){ alert(123); return 8 } var ret = func1() alert(ret) 方 ...

  5. ASP 基础二 内置对象

    一 Request 二 Response 三 Application 四 Session 五 Server <script language="vbscript" runat ...

  6. TP框架基础 (二) ---空控制器和空操作

    通过之前的学习我们知道了index.php是一个入口文件,如果没有这个入口文件的话,我们需要自己创建! [视图模板文件创建] 视图模板文件存放发位置在: 里面没有模板文件 如果我们想要访问Login控 ...

  7. Bootstrap <基础二十九>面板(Panels)

    Bootstrap 面板(Panels).面板组件用于把 DOM 组件插入到一个盒子中.创建一个基本的面板,只需要向 <div> 元素添加 class .panel 和 class .pa ...

  8. Bootstrap <基础二十八>列表组

    列表组.列表组件用于以列表形式呈现复杂的和自定义的内容.创建一个基本的列表组的步骤如下: 向元素 <ul> 添加 class .list-group. 向 <li> 添加 cl ...

  9. Bootstrap <基础二十六>进度条

    Bootstrap 进度条.在本教程中,你将看到如何使用 Bootstrap 创建加载.重定向或动作状态的进度条. Bootstrap 进度条使用 CSS3 过渡和动画来获得该效果.Internet ...

  10. Bootstrap <基础二十五>警告(Alerts)

    警告(Alerts)以及 Bootstrap 所提供的用于警告的 class.警告(Alerts)向用户提供了一种定义消息样式的方式.它们为典型的用户操作提供了上下文信息反馈. 您可以为警告框添加一个 ...

随机推荐

  1. 内核源码中单个.o文件的编译过程(六)

    通过对过渡篇的学习,相信你已经具有了相当的知识储备,接下来就来继续学习单个.o文件的编译过程 以/drivers/char/mem.c的编译为例 make /drivers/char/mem.o 一. ...

  2. React报错:Module not found: Error: Can't resolve 'react-router-dom'

    解决方案 npm install -S react-router-dom@5 参考链接 https://stackoverflow.com/questions/53914013/failed-to-c ...

  3. 从一道面试题来谈谈Golang中的 ==

    写这篇文章的时候,已经离我找工作有一段时间了,但是觉得这道题不管是面试还是日常的工作中,都会经常遇到,所以还是特意写一篇文章,记录下自己对Golang中==的理解.如文章中出现不对的地方,请不吝赐教, ...

  4. [jmeter]快速入门

    前言 以压测一个api为例.假设有一个非常简单的api http://192.168.1.111:8080/,只是返回 helloworld 字符串. 准备 打开 jmeter,新建测试计划 在测试计 ...

  5. 【LaTeX】语法(更新中)

    目录 长度 空行 空格 超链接 数学公式 段落中(隐式) 单独成段(显式) 居中,左对齐,右对齐 居中 左对齐 右对齐 参考文献配置 TODO 参考资料 中文支持参考环境配置中的 内容,在这里不做重复 ...

  6. 深入理解Linux内核——内存管理(3)

    提要:本系列文章主要参考MIT 6.828课程以及两本书籍<深入理解Linux内核> <深入Linux内核架构>对Linux内核内容进行总结. 内存管理的实现覆盖了多个领域: ...

  7. springboot项目自动关闭进程重启脚本

    话不多说,先上脚本 kill -15 $(netstat -nlp | grep :9095 | awk '{print $7}' | awk -F"/" '{ print $1 ...

  8. MindSponge分子动力学模拟——Constraint约束

    技术背景 在前面的几篇博客中,我们已经介绍了MindSponge的基本使用方法,比如定义一个分子系统.计算分子的单点能以及迭代器的使用等.有了这些基础的教程,用户以及可以执行一些比较简单的模拟任务,比 ...

  9. 在CentOS 8上安装Xrdp远程桌面服务

    如何在CentOS 8上安装Xrdp远程桌面服务 写在前面 Xrdp是Microsoft远程桌面协议(RDP)的开源实现,可让您以图形方式控制远程系统.使用RDP,您可以登录到远程计算机并创建真实的桌 ...

  10. xv6 进程切换中的锁:MIT6.s081/6.828 lectrue12:Coordination 以及 Lab6 Thread 心得

    引言 这节课和上一节xv6进程切换是一个完整的的进程切换专题,上一节主要讨论进程切换过程中的细节,而这一节主要讨论进程切换过程中锁的使用,所以本节的两大关键词就是"Coordination& ...