本节介绍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. 聊聊Spring注解@Transactional失效的那些事

    一.前言 emm,又又又踩坑啦.这次的需求主要是对逾期计算的需求任务进行优化,现有的计算任务运行时间太长了.简单描述下此次的问题:在项目中进行多个数据库执行操作时,我们期望的是将其整个封装成一个事务, ...

  2. nginx配置文件内容(1)

    nginx.conf内容 在Nginx服务器的主配置文件nginx.conf中,包括全局配置.I/O事件配置.HTTP配置这三大块内容,配置语句的格式为"关键字  值:"(末尾以分 ...

  3. C++(继承)

    继承 struct Person { int age; int sex; }; struct Teacher { int age; int sex; int level; int classId; } ...

  4. Blazor 跨平台的、共享一套UI的天气预报 Demo

    1. 前言 很久之前就读过 dotnet9 大佬的一篇文章,MAUI与Blazor共享一套UI,媲美Flutter,实现Windows.macOS.Android.iOS.Web通用UI,没读过的可以 ...

  5. redis 中的 字符串

    String是redis 中的最基本的类型, 为二进制安全  ,意味着String可以表示各种类型  一个字符串value 最大为 521M set k1 v100 set k2 v200 get 命 ...

  6. 渗透-02:HTTPS主干-分支和HTTPS传输过程

    一.HTTPS主干-分支 第一层 第一层,是主干的主干,加密通信就是双方都持有一个对称加密的秘钥,然后就可以安全通信了. 问题就是,无论这个最初的秘钥是由客户端传给服务端,还是服务端传给客户端,都是明 ...

  7. 面霸的自我修养:synchronized专题

    王有志,一个分享硬核Java技术的互金摸鱼侠 加入Java人的提桶跑路群:共同富裕的Java人 今天是<面霸的自我修养>的第3弹,内容是Java并发编程中至关重要的关键字synchroni ...

  8. 纯分享:将MySql的建表DDL转为PostgreSql的DDL

    背景 现在信创是搞得如火如荼,在这个浪潮下,数据库也是从之前熟悉的Mysql换到了某国产数据库. 该数据库我倒是想吐槽吐槽,它是基于Postgre 9.x的基础上改的,至于改了啥,我也没去详细了解,当 ...

  9. 手把手教你使用Vite构建第一个Vue3项目

    写在前面 在之前的文章中写过"如何创建第一个vue项目",但那篇文章写的是创建vue2的 项目. 传送门如何创建第一个vue项目 打开Vue.js官网:https://cn.vue ...

  10. Win10 误删winsock注册表修复。 winsock.reg

    手贱删除了注册表的winsock项, 导致无法上网. 导入后需要重启电脑才能上网, 这个文件是我在别人电脑里导出来的. 下载地址: https://pan.baidu.com/s/1wH8SdeWsx ...