本节介绍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. 上班第一天 Android 环境配置

    其实是昨天把大概 回归Android开发第一天 学会查 然后等待 反正我是不希望以后再查了 写出来吧 去谷歌那边把android studio下载下来 更新jdk版本(与传统的java开发不同 高版本 ...

  2. 2023-07-20:假设一共有M个车库,编号1~M,时间点从早到晚是从1~T, 一共有N个记录,每一条记录如下{a, b, c}, 表示一辆车在b时间点进入a车库,在c时间点从a车库出去, 一共有K

    2023-07-20:假设一共有M个车库,编号1 ~ M,时间点从早到晚是从1 ~ T, 一共有N个记录,每一条记录如下{a, b, c}, 表示一辆车在b时间点进入a车库,在c时间点从a车库出去, ...

  3. Error: Could not open client transport with JDBC Uri: jdbc:hive2://localhost:10000: java.net.ConnectException: 拒绝连接 (Connection refused) (state=08S01,code=0)

    一:启动hiveserver2服务 二:启动beeline 三:连接hiveserver2(下面的1000000端口号适当改小写因为其超出最大端口号的范围建议改为10000) 如果启动不成功实现我们先 ...

  4. zabbix 修改模板中单个主机的触发器

    参考文档:zabbix 修改模板中单个主机的触发器 在主机的 Triggers,克隆后修改,再disable原来的触发器.

  5. Python 潮流周刊第 14 期(内容摘要)

    你好,我是猫哥.这里每周分享优质的 Python.AI 及通用技术内容,本期分享的全部是英文材料. 本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程. ...

  6. jQuery项目的小技巧

    1.返回顶部按钮 你可以利用 animate 和 scrollTop 来实现返回顶部的动画,而不需要使用其他插件. // Back to top // Back to top $('a.top').c ...

  7. Mysql高级8-触发器

    一.触发器 触发器是与表有关的数据库对象,指在insert/update/delete之前或者之后,触发并执行触发器中定义的sql语句集合,触发器的这种特性可以协助应用在数据库端确保数据的完整性,日志 ...

  8. 将实体类对象数据存入和读取进csv文件(可追加)

    前言 最近公司一个新的项目,因为需要存储的数据很少,单独去部署一个数据库去存储该数据显然是不划算的,所以想的是通过存入csv文件中来代替存入数据库中.说干就干. 什么是csv文件 CSV代表逗号分隔值 ...

  9. 2、搭建MyBatis

    2.1.开发环境 IDE:idea 2019.2 构建工具:maven 3.8.4 MySQL版本:MySQL 5.7 MyBatis版本:MyBatis 3.5.7 MySQL不同版本的注意事项 ( ...

  10. P2024 [NOI2001] 食物链 || #576. 食物链【NOI2001】 (并查集)

    空降锣鼓 空降OJ 题解: #include<bits/stdc++.h> using namespace std; int n,k; int d,x,y; int ans; int fa ...