AutoMapper 最佳实践
AutoMapper 是一个基于命名约定的对象->对象映射工具。
只要2个对象的属性具有相同名字(或者符合它规定的命名约定),AutoMapper就可以替我们自动在2个对象间进行属性值的映射。如果有不符合约定的属性,或者需要自定义映射行为,就需要我们事先告诉AutoMapper,所以在使用 Map(src,dest)进行映射之前,必须使用 CreateMap() 进行配置。
Mapper.CreateMap<Product, ProductDto>(); // 配置
Product entity = Reop.FindProduct(id); // 从数据库中取得实体
Assert.AreEqual("挖掘机", entity.ProductName);
ProductDto productDto = Mapper.Map(entity); // 使用AutoMapper自动映射
Assert.AreEqual("挖掘机", productDto.ProductName);
AutoMapper就是这样一个只有2个常用函数的简单方便的工具。不过在实际使用时还是有一些细节需要注意,下面将把比较重要的罗列出来。PS:项目的ORM框架是NHibernate。
1. 在程序启动时执行所有的AutoMapper配置,并且把映射代码放置到一起
下面是一个典型的AutoMapper全局配置代码,里面的一些细节会在后面逐一解释。
public class DtoMapping
{
private readonly IContractReviewMainAppServices IContractReviewMainAppServices;
private readonly IDictionaryAppService IDictionaryAppService;
private readonly IProductAppService IProductAppService;
public DtoMapping(IContractReviewMainAppServices IContractReviewMainAppServices,
IDictionaryAppService IDictionaryAppService, IProductAppService IProductAppService)
{
this.IContractReviewMainAppServices = IContractReviewMainAppServices;
this.IDictionaryAppService = IDictionaryAppService;
this.IProductAppService = IProductAppService;
} public void InitMapping()
{
#region 合同购买设备信息
Mapper.CreateMap<ContractReviewProduct, ContractReviewProductDto>();
Mapper.CreateMap<ContractReviewProductDto, ContractReviewProduct>() // DTO 向 Entity 赋值
.ForMember(entity => entity.ContractReviewMain, opt => LoadEntity(opt,
dto => dto.ContractReviewMainId,
IContractReviewMainAppServices.Get))
.ForMember(entity => entity.DeviceCategory, opt => LoadEntity(opt,
dto => dto.DeviceCategoryId,
IDictionaryAppService.FindDicItem))
.ForMember(entity => entity.DeviceName, opt => LoadEntity(opt,
dto => dto.DeviceNameId,
IProductAppService.FindProduct))
.ForMember(entity => entity.ProductModel, opt => LoadEntity(opt,
dto => dto.ProductModelId,
IProductAppService.FindProduct))
.ForMember(entity => entity.Unit, opt => LoadEntity(opt,
dto => dto.UnitId,
IDictionaryAppService.FindDicItem))
.ForMember(entity => entity.Creator, opt => opt.Ignore()); // DTO 里面没有的属性直接Ignore
#endregion 合同购买设备信息 #region 字典配置
Mapper.CreateMap<DicCategory, DicCategoryDto>();
Mapper.CreateMap<DicCategoryDto, DicCategory>();
Mapper.CreateMap<DicItem, DicItemDto>();
Mapper.CreateMap<DicItemDto, DicItem>()
.ForMember(entity => entity.Category, opt => LoadEntity(opt,
dto => dto.CategoryId,
IDictionaryAppService.FindDicCategory));
#endregion 字典配置 // 对于所有的 DTO 到 Entity 的映射,都忽略 Id 和 Version 属性
IgnoreDtoIdAndVersionPropertyToEntity(); // 验证配置
Mapper.AssertConfigurationIsValid();
} /// <summary>
/// 加载实体对象。
/// <remarks>Id是null的会被忽略;Id是string.Empty的将被赋值为null;Id是GUID的将从数据库中加载并赋值。</remarks>
/// </summary>
/// <typeparam name="TSource"></typeparam>
/// <typeparam name="TMember"></typeparam>
/// <param name="opt"></param>
/// <param name="getId"></param>
/// <param name="doLoad"></param>
private void LoadEntity<TSource, TMember>(IMemberConfigurationExpression<TSource> opt,
Func<TSource, string> getId, Func<string, TMember> doLoad) where TMember : class
{
opt.Condition(src => (getId(src) != null));
opt.MapFrom(src => getId(src) == string.Empty ? null : doLoad(getId(src)));
} /// <summary>
/// 对于所有的 DTO 到 Entity 的映射,都忽略 Id 和 Version 属性
/// <remarks>当从DTO向Entity赋值时,要保持从数据库中加载过来的Entity的Id和Version属性不变!</remarks>
/// </summary>
private void IgnoreDtoIdAndVersionPropertyToEntity()
{
PropertyInfo idProperty = typeof(Entity).GetProperty("Id");
PropertyInfo versionProperty = typeof(Entity).GetProperty("Version");
foreach (TypeMap map in Mapper.GetAllTypeMaps())
{
if (typeof(Dto).IsAssignableFrom(map.SourceType)
&& typeof(Entity).IsAssignableFrom(map.DestinationType))
{
map.FindOrCreatePropertyMapFor(new PropertyAccessor(idProperty)).Ignore();
map.FindOrCreatePropertyMapFor(new PropertyAccessor(versionProperty)).Ignore();
}
}
}
}
DTO 与 Entity 之间的 AutoMapper全局配置代码
虽然AutoMapper并不强制要求在程序启动时一次性提供所有配置,但是这样做有如下好处:
a) 可以在程序启动时对所有的配置进行严格的验证(后文详述)。
b) 可以统一指定DTO向Entity映射时的通用行为(后文详述)。
c) 逻辑内聚:新增配置时方便模仿以前写过的配置;对项目中一共有多少DTO以及它们与实体的映射关系也容易有直观的把握。
2. 在程序启动时对所有的配置进行严格的验证
AutoMapper并不强制要求执行 Mapper.AssertConfigurationIsValid() 验证目标对象的所有属性都能找到源属性(或者在配置时指定了默认映射行为)。换句话说,即使执行 Mapper.AssertConfigurationIsValid() 验证失败了调用 Mapper() 也能成功映射(找不到源属性的目标属性将被赋默认值)。但是我们仍然应该在程序启动时对所有的配置进行严格的验证,并且在验证失败时立即找出原因并进行处理。因为我们在创建DTO时有可能因为手误造成DTO的属性与Entity的属性名称不完全一样;或者当Entity被重构,造成Entity与DTO不完全匹配,这将造成许多隐性Bug,难以察觉,难以全部根除,这也是DTO经常被人诟病的一大缺点。使用AutoMapper的验证机制可以从根本上消除这一隐患,所以即使麻烦一点也要一直坚持进行验证。
3. 指定DTO向Entity映射时的通用行为
从DTO对象向Entity对象映射时,应该是先从数据库中加载Entity对象,然后把DTO对象的属性值覆盖到Entity对象中。Entity对象的Id和Version属性要么是从数据库中加载的(更新时),要么是由Entity对象自主获取的默认值(新增时),无论哪种情况,都不应该让DTO里的属性值覆盖到Entity里的这2个属性。
Mapper.CreateMap<DicCategoryDto, DicCategory>()
.ForMember(entity => entity.Id, opt => opt.Ignore())
.ForMember(entity => entity.Version, opt => opt.Ignore());
但是每个DTO到Entity的配置都这么写一遍的话,麻烦不说,万一忘了后果不堪设想。通过在配置的最后调用IgnoreDtoIdAndVersionPropertyToEntity()函数可以统一设置所有DTO向Entity的映射都忽略Id和Version属性。
/// <summary>
/// 对于所有的 DTO 到 Entity 的映射,都忽略 Id 和 Version 属性
/// <remarks>当从DTO向Entity赋值时,要保持从数据库中加载过来的Entity的Id和Version属性不变!</remarks>
/// </summary>
private void IgnoreDtoIdAndVersionPropertyToEntity()
{
PropertyInfo idProperty = typeof(Entity).GetProperty("Id");
PropertyInfo versionProperty = typeof(Entity).GetProperty("Version");
foreach (TypeMap map in Mapper.GetAllTypeMaps())
{
if (typeof(Dto).IsAssignableFrom(map.SourceType)
&& typeof(Entity).IsAssignableFrom(map.DestinationType))
{
map.FindOrCreatePropertyMapFor(new PropertyAccessor(idProperty)).Ignore();
map.FindOrCreatePropertyMapFor(new PropertyAccessor(versionProperty)).Ignore();
}
}
}
另一方案:下面这种写法是官方推荐的,可读性更好,但是实测Ignore()选项并没有生效!不知道是不是Bug。
Mapper.CreateMap<Dto, Entity>()
.ForMember(entity => entity.Id, opt => opt.Ignore())
.ForMember(entity => entity.Version, opt => opt.Ignore())
.Include<ContractReviewProductDto, ContractReviewProduct>()
.Include<DicCategoryDto, DicCategory>()
.Include<DicItemDto, DicItem>();
不好用的代码
4. 通过配置实现DTO向Entity映射时加载实体
从DTO向Entity映射时,如果Entity有关联的属性,需要调用NHibernate的LoadEntity()根据Client传过来的关联属性Id加载实体对象。这项工作很适合放到AutoMapper的配置代码里。进一步地,我们可以约定:关联属性Id是null时,表示忽略此属性;如果关联属性Id是string.Empty,表示要把此属性置空;如果关联属性Id是GUID,则加载实体对象。然后,把这个逻辑抽取出来形成 LoadEntity() 函数以避免冗余代码。
/// <summary>
/// 加载实体对象。
/// <remarks>Id是null的会被忽略;Id是string.Empty的将被赋值为null;Id是GUID的将从数据库中加载并赋值。</remarks>
/// </summary>
private void LoadEntity<TSource, TMember>(IMemberConfigurationExpression<TSource> opt,
Func<TSource, string> getId, Func<string, TMember> doLoad) where TMember : class
{
opt.Condition(src => (getId(src) != null));
opt.MapFrom(src => getId(src) == string.Empty ? null : doLoad(getId(src)));
}
这样在配置的时候就可以使用声明式的代码了:
Mapper.CreateMap<ContractReviewProductDto, ContractReviewProduct>() // DTO 向 Entity 赋值
.ForMember(entity => entity.DeviceCategory, opt => LoadEntity(opt,
dto => dto.DeviceCategoryId,
IDictionaryAppService.FindDicItem))
5. 让AutoMapper合并2个对象而不是创建新对象
Map()方法有2种使用方式。一种是由AutoMapper创建目标对象:
ProductDto dto = Mapper.Map<Product, ProductDto>(entity);
另一种是让AutoMapper把源对象中的属性值合并/覆盖到目标对象:
ProductDto dto = new ProductDto();
Maper.Map(entity, dto);
应该总是使用后一种。对于Entity向DTO映射的情况,由于有时候需要把2个Entity对象映射到一个DTO对象中,所以应该使用后一种方式。对于DTO向Entity映射的情况,需要先从数据库中加载Entity对象,再把DTO对象中的部分属性值覆盖到Entity对象中。
6. 考虑通过封装让AutoMapper可被取消和可替换
当我们使用外部工具的时候,一般总要想写办法尽量使这些工具容易被取消和替换,以避免技术风险,同时还能保证以更统一的方式使用工具。由于DTO对Entity是不可见的,所以Entity到DTO的映射和DTO到Entity的映射方法都要添加到DTO的基类中。注意我们没有使用Map()方法的泛型版本,这样便于增加新的抽象DTO基类,例如业务对象的DTO基类BizInfoDto。
/// <summary>
/// 数据传输对象抽象类
/// </summary>
public abstract class Dto
{
/// <summary>
/// 从实体中取得属性值
/// </summary>
/// <param name="entity"></param>
public virtual void FetchValuesFromEntity<TEntity>(TEntity entity)
{
Mapper.Map(entity, this, entity.GetType(), this.GetType());
} /// <summary>
/// 将DTO中的属性值赋值到实体对象中
/// </summary>
/// <param name="entity"></param>
public virtual void AssignValuesToEntity<TEntity>(TEntity entity)
{
Mapper.Map(this, entity, this.GetType(), entity.GetType());
} [Description("主键Id")]
public string Id { get; set; } [Description("版本号")]
public int Version { get; set; }
} /// <summary>
/// 业务DTO基类
/// </summary>
public abstract class BizInfoDto : Dto
{
[Description("删除标识")]
public bool Del { get; set; } [Description("最后更新时间")]
public DateTime? UpdateTime { get; set; } [Description("数据产生时间")]
public DateTime? CreateTime { get; set; }
}
DTO基类代码
然后像这样使用:
dto.AssignValuesToEntity(entity);
dto.FetchValuesFromEntity(entity);
再为IList添加用于映射的扩展方法,用于将Entity列表映射为DTO列表:
public static class AutoMapperCollectionExtension
{
public static IList<TDto> ToDtoList<TEntity, TDto>(this IList<TEntity> entityList)
{
return Mapper.Map<IList<TEntity>, IList<TDto>>(entityList);
}
}
7. 使用扁平化的双向DTO
AutoMapper能够非常便利地根据命名约定生成扁平化的DTO。从DTO向Entity映射时,需要配置根据属性Id加载实体的方法,在前文[4. 通过配置实现DTO向Entity映射时加载实体]有详细描述。
粒度过细的DTO不利于管理。一般一个扁平化的双向DTO就可以应付大多数场景了。扁平化的DTO不但可以让Client端得到更为简单的数据结构,节省流量,同时也是非常棒的解除循环引用的方案,方便Json序列化(后文详述)。
8. 使用扁平化消除循环引用
AutoMapper在技术上是支持把带有循环引用的Entity对象映射为同样具有循环引用关系的DTO对象的。但是带有循环应用的DicCategoryDto对象在进一步Json序列化时,DicItemDto的Category属性就会因为循环引用而被丢弃了。而像上图那样把多端扁平化,就可以仍然保留我们感兴趣的Category属性的信息了。
9. 将DTO放置在Service层
原则上Entity应该不知道DTO,所以物理上也最好把DTO放置在Service层里面。但是有一个技术问题:有时候需要在Repository层里面让NHibernate执行原生SQL语句,然后就需要利用NHibernate的AliasToBean()方法将查询结果映射到DTO对象里面。如果DTO放置在Service层里面,该怎么把DTO的类型传递给Repository层呢?下面将给出2种解决方案。
9.1 利用泛型将Service层的DTO类型传递给Repository层
下面是一个在Repository层使用NHibernate执行原生SQL的例子,利用泛型指定DTO的类型。
public IList<TDto> GetRawSqlList<TDto>()
{
var query = Session.CreateSQLQuery(@"SELECT max(cg.TEXT) as ProductCategory, sum(p.COUNT_NUM) as TotalNum
FROM CNT_RW_PRODUCT p
left join SYS_DIC_ITEM cg on p.CATEGORY = cg.DIC_ITEM_ID
where p.DEL = :DEL
group by p.CATEGORY")
.SetBoolean("DEL", false);
query.SetResultTransformer(NHibernate.Transform.Transformers.AliasToBean<TDto>());
return query.List<TDto>();
}
然后,在Service层创建一个与查询结果匹配的DTO:
public class ProductCategorySummaryDto : Dto
{
[Description("产品类别")]
public string ProductCategory { get; set; } [Description("总数量")]
public int TotalNum { get; set; }
}
在Service层的GetRawSQLResult()方法的定义:
public IList<ProductCategorySummaryDto> GetRawSQLResult()
{
return IContractReviewProductRepository.GetRawSqlList<ProductCategorySummaryDto>();
}
9.2 另一方案:使用ExpandoObject对象返回查询结果
如果查询结果只使用一次,单独为它创建一个DTO成本似乎有些过高。下面同样是在Repository利用NHibernate执行原生SQL,但是返回值是一个动态对象的列表。
public IList<dynamic> GetExpandoObjectList(string contractReviewMainId)
{
var query = Session.CreateQuery(@"select t.Id as Id,
t.Version as Version,
t.Place as Place,
t.DeviceName.Text as DeviceNameText,
t.DeviceName.Id as DeviceNameId
from ContractReviewProduct t
where t.ContractReviewMain.Id = :ContractReviewMainId")
.SetAnsiString("ContractReviewMainId", contractReviewMainId);
return query.DynamicList();
}
注意DynamicList()方法是一个自定义的扩展方法:
public static class NHibernateExtensions
{
public static IList<dynamic> DynamicList(this IQuery query)
{
return query.SetResultTransformer(NhTransformers.ExpandoObject)
.List<dynamic>();
}
} public static class NhTransformers
{
public static readonly IResultTransformer ExpandoObject; static NhTransformers()
{
ExpandoObject = new ExpandoObjectResultSetTransformer();
} private class ExpandoObjectResultSetTransformer : IResultTransformer
{
public IList TransformList(IList collection)
{
return collection;
} public object TransformTuple(object[] tuple, string[] aliases)
{
var expando = new ExpandoObject();
var dictionary = (IDictionary<string, object>)expando;
for (int i = ; i < tuple.Length; i++)
{
string alias = aliases[i];
if (alias != null)
{
dictionary[alias] = tuple[i];
}
}
return expando;
}
}
}
DynamicList()扩展方法和ExpandoObjectResultSetTransformer
在Service层使用返回的动态对象的代码与使用普通代码看上去一样。也可以直接把返回的动态对象利用Json.Net序列化。
[TestMethod]
public void TestGetExpandoObject()
{
IList<dynamic> result = IContractReviewProductRepository().GetExpandoObjectList("5AB17F4D-803E-4641-8FCF-660662458BAA"); Assert.AreEqual("刮板机", result[].DeviceNameText);
Assert.AreEqual(, result[].Version);
}
但是本质上ExpandoObject只是一个IDictionary。目前AutoMapper3.1还不支持把ExpandoObject对象映射成普通对象。没有编译期的语法检查,没有类型信息,没有静态的属性信息,将来想重构都十分不便。曾经非常羡慕Ruby等动态语言的灵活和便利,但是当C#向着动态语言大踏步前进时,反而有些感到害怕了。
AutoMapper 最佳实践的更多相关文章
- AutoMapper 使用实践
一. 使用意图 常常在开发过程中,碰到一个实体上的属性值,要赋值给另外一个相类似实体属性时,且属性有很多的情况.一般不利用工具的话,就要实例化被赋值实体B,然后再将实体A的字段一个个赋值给B的属性 ...
- mybatis 3.x源码深度解析与最佳实践(最完整原创)
mybatis 3.x源码深度解析与最佳实践 1 环境准备 1.1 mybatis介绍以及框架源码的学习目标 1.2 本系列源码解析的方式 1.3 环境搭建 1.4 从Hello World开始 2 ...
- [转]ASP.NET MVC 4 最佳实践宝典
原文:http://www.cnblogs.com/sonykings/archive/2013/05/30/3107531.html ASP.NET MVC最佳实践 本文档提供了一套旨在帮助创建最佳 ...
- ABP vnext模块化架构的最佳实践的实现
在上一篇文章<手把手教你用Abp vnext构建API接口服务>中,我们用ABP vnext实现了WebAPI接口服务,但是并非ABP模块化架构的最佳实践.我本身也在学习ABP,我认为AB ...
- 基于ABP落地领域驱动设计-05.实体创建和更新最佳实践
目录 系列文章 数据传输对象 输入DTO最佳实践 不要在输入DTO中定义不使用的属性 不要重用输入DTO 输入DTO中验证逻辑 输出DTO最佳实践 对象映射 学习帮助 系列文章 基于ABP落地领域驱动 ...
- ASP.NET跨平台最佳实践
前言 八年的坚持敌不过领导的固执,最终还是不得不阔别已经成为我第二语言的C#,转战Java阵营.有过短暂的失落和迷茫,但技术转型真的没有想象中那么难.回头审视,其实单从语言本身来看,C#确实比Java ...
- 《AngularJS深度剖析与最佳实践》简介
由于年末将至,前阵子一直忙于工作的事务,不得已暂停了微信订阅号的更新,我将会在后续的时间里尽快的继续为大家推送更多的博文.毕竟一个人的力量微薄,精力有限,希望大家能理解,仍然能一如既往的关注和支持sh ...
- ASP.NET MVC防范CSRF最佳实践
XSS与CSRF 哈哈,有点标题党,但我保证这篇文章跟别的不太一样. 我认为,网站安全的基础有三块: 防范中间人攻击 防范XSS 防范CSRF 注意,我讲的是基础,如果更高级点的话可以考虑防范机器人刷 ...
- 快速web开发中的前后端框架选型最佳实践
这个最佳实践是我目前人在做的一个站点,主要功能: oauth登录 发布文章(我称为"片段"),片段可以自定义一些和内容有关的指标,如“文中人物:12”.支持自定义排版.插图.建立相 ...
随机推荐
- configuration error-could not load file or assembly crystaldecisions.reportappserver.clientdoc
IIS启动网站后报错: configuration error Could not load file or assembly 'crystaldecisions.reportappserver.cl ...
- AC自动机专题
AC自动机简介:KMP是用于解决单模式串匹配问题, AC自动机用于解决多模式串匹配问题. 精华:设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个节点,他的儿子中也有字母为C的节点.然后把当 ...
- Beginning Scala study note(4) Functional Programming in Scala
1. Functional programming treats computation as the evaluation of mathematical and avoids state and ...
- Stack Overflow: The Architecture - 2016 Edition(Translation)
原文: https://nickcraver.com/blog/2016/02/17/stack-overflow-the-architecture-2016-edition/ 作者:Nick Cra ...
- httpclient 无信任证书使用https
1.当不需要使用任何证书访问https网页时,只需配置信任任何证书 HttpClient http = new HttpClient(); String url = "https://pay ...
- maven打包时,依赖包打不进jar包中
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/20 ...
- JS代码判断IE6,IE7,IE8,IE9!
JS代码判断IE6,IE7,IE8,IE9!2011年12月15日 星期四 14:01做网页有时候会用到JS检测IE的版本,下面是检测Microsoft Internet Explorer版本的三种代 ...
- css的margin
1.适合于没有设定width/height的普通block水平元素 2.只适用于水平方向尺寸 例子:一侧定宽的自适应布局 <html> <head> <meta name ...
- 弱省互测#0 t3
Case 1 题意 要求给出下面代码的答案然后构造输入. 给一个图, n 个点 m 条边 q 次询问,输出所有点对之间最大权值最小的路径. 题解 把每一个询问的输出看成一条边,建一棵最小生成树. Ca ...
- 使用logrotate分割Tomcat的catalina日志
切割catalian日志的方式有很多种,比如shell脚本的.cronolog或者通过配置log4j等等这些都可以实现.但今天我要介绍是使用logrotate来按日期生成日志. 原文是一个外国博主的文 ...