本节介绍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 提供了 MapTo 和 MapToList 扩展方法.
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();