AutoMapper 最佳实践

AutoMapper 最佳实践AutoMapper nbsp 是一个基于命名约定的对象 对象映射工具 只要 2 个对象的属性具有相同名字 或者符合它规定的命名约定 AutoMapper 就可以替我们自动在 2 个对象间进行属性值的映射 如果有不符合约定的属性 或者需要自定义映射行为 就需要我们事先告诉 AutoMapper 所以在使用 Map src dest 进行映射之前 必须使用 CreateMap 进行配置 Mappe

AutoMapper
 是一个基于命名约定的对象->对象映射工具。

  只要2个对象的属性具有相同名字(或者符合它规定的命名约定),AutoMapper就可以替我们自动在2个对象间进行属性值的映射。如果有不符合约定的属性,或者需要自定义映射行为,就需要我们事先告诉AutoMapper,所以在使用 Map(src,dest)进行映射之前,必须使用 CreateMap() 进行配置。

Mapper.CreateMap 
   
     (); // 配置 Product entity = Reop.FindProduct(id); // 从数据库中取得实体 Assert.AreEqual("挖掘机", entity.ProductName); ProductDto productDto = Mapper.Map(entity); // 使用AutoMapper自动映射 Assert.AreEqual("挖掘机", productDto.ProductName); 
   

下面是一个典型的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 
    
      (); Mapper.CreateMap 
     
       () // 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 
      
        (); Mapper.CreateMap 
       
         (); Mapper.CreateMap 
        
          (); Mapper.CreateMap 
         
           () .ForMember(entity => entity.Category, opt => LoadEntity(opt, dto => dto.CategoryId, IDictionaryAppService.FindDicCategory)); #endregion 字典配置 // 对于所有的 DTO 到 Entity 的映射,都忽略 Id 和 Version 属性 IgnoreDtoIdAndVersionPropertyToEntity(); // 验证配置 Mapper.AssertConfigurationIsValid(); } /// 
           /// 加载实体对象。 /// 
           
             Id是null的会被忽略;Id是string.Empty的将被赋值为null;Id是GUID的将从数据库中加载并赋值。 
            ///  /// 
           /// 
           /// 
           ///  ///  private void LoadEntity 
             
               (IMemberConfigurationExpression 
              
                opt, Func 
               
                 getId, Func 
                
                  doLoad) where TMember : class { opt.Condition(src => (getId(src) != null)); opt.MapFrom(src => getId(src) == string.Empty ? null : doLoad(getId(src))); } /// 
                  /// 对于所有的 DTO 到 Entity 的映射,都忽略 Id 和 Version 属性 /// 
                  
                    当从DTO向Entity赋值时,要保持从数据库中加载过来的Entity的Id和Version属性不变! 
                   ///  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 
             
               () .ForMember(entity => entity.Id, opt => opt.Ignore()) .ForMember(entity => entity.Version, opt => opt.Ignore()); 
             

但是每个DTO到Entity的配置都这么写一遍的话,麻烦不说,万一忘了后果不堪设想。通过在配置的最后调用IgnoreDtoIdAndVersionPropertyToEntity()函数可以统一设置所有DTO向Entity的映射都忽略Id和Version属性。

///  /// 对于所有的 DTO 到 Entity 的映射,都忽略 Id 和 Version 属性 /// 
              
                当从DTO向Entity赋值时,要保持从数据库中加载过来的Entity的Id和Version属性不变! 
               ///  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 
              
                () .ForMember(entity => entity.Id, opt => opt.Ignore()) .ForMember(entity => entity.Version, opt => opt.Ignore()) .Include 
               
                 () .Include 
                
                  () .Include 
                 
                   (); 不好用的代码 
                  
                 
                
              

4. 通过配置实现DTO向Entity映射时加载实体
从DTO向Entity映射时,如果Entity有关联的属性,需要调用NHibernate的LoadEntity()根据Client传过来的关联属性Id加载实体对象。这项工作很适合放到AutoMapper的配置代码里。进一步地,我们可以约定:关联属性Id是null时,表示忽略此属性;如果关联属性Id是string.Empty,表示要把此属性置空;如果关联属性Id是GUID,则加载实体对象。然后,把这个逻辑抽取出来形成 LoadEntity() 函数以避免冗余代码。

///  /// 加载实体对象。 /// 
                 
                   Id是null的会被忽略;Id是string.Empty的将被赋值为null;Id是GUID的将从数据库中加载并赋值。 
                  ///  private void LoadEntity 
                
                  (IMemberConfigurationExpression 
                 
                   opt, Func 
                  
                    getId, Func 
                   
                     doLoad) where TMember : class { opt.Condition(src => (getId(src) != null)); opt.MapFrom(src => getId(src) == string.Empty ? null : doLoad(getId(src))); } 
                    
                   
                  
                

这样在配置的时候就可以使用声明式的代码了:

Mapper.CreateMap 
                 
                   () // DTO 向 Entity 赋值 .ForMember(entity => entity.DeviceCategory, opt => LoadEntity(opt, dto => dto.DeviceCategoryId, IDictionaryAppService.FindDicItem)) 
                 

5. 让AutoMapper合并2个对象而不是创建新对象
Map()方法有2种使用方式。一种是由AutoMapper创建目标对象:
ProductDto dto = Mapper.Map (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。

///  /// 数据传输对象抽象类 ///  public abstract class Dto { ///  /// 从实体中取得属性值 ///  ///  public virtual void FetchValuesFromEntity 
                               
                                 (TEntity entity) { Mapper.Map(entity, this, entity.GetType(), this.GetType()); } /// 
                                 /// 将DTO中的属性值赋值到实体对象中 ///  /// 
                                 public virtual void AssignValuesToEntity 
                                 
                                   (TEntity entity) { Mapper.Map(this, entity, this.GetType(), entity.GetType()); } [Description("主键Id")] public string Id { get; set; } [Description("版本号")] public int Version { get; set; } } /// 
                                   /// 业务DTO基类 ///  public abstract class BizInfoDto : Dto { [Description("删除标识")] public bool Del { get; set; } [Description("最后更新时间")] public DateTime? UpdateTime { get; set; } [Description("数据产生时间")] public DateTime? CreateTime { get; set; } } DTO基类代码 
                                   
                               

然后像这样使用:

public static class AutoMapperCollectionExtension { public static IList 
                               
                                 ToDtoList 
                                
                                  (this IList 
                                 
                                   entityList) { return Mapper.Map 
                                  
                                    , IList 
                                   
                                     >(entityList); } } 
                                    
                                   
                                  
                                 
                               

7. 使用扁平化的双向DTO
AutoMapper 最佳实践
AutoMapper能够非常便利地根据命名约定生成扁平化的DTO。从DTO向Entity映射时,需要配置根据属性Id加载实体的方法,在前文[4. 通过配置实现DTO向Entity映射时加载实体]有详细描述。
  粒度过细的DTO不利于管理。一般一个扁平化的双向DTO就可以应付大多数场景了。扁平化的DTO不但可以让Client端得到更为简单的数据结构,节省流量,同时也是非常棒的解除循环引用的方案,方便Json序列化(后文详述)。

8. 使用扁平化消除循环引用
AutoMapper 最佳实践
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
                                                
  
                                                
                                                
                                                
                                                
                                                
                                                  GetRawSqlList 
                                                 
                                                   () { 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 
                                                  
                                                    ()); return query.List 
                                                   
                                                     (); } 
                                                    
                                                   
                                                  
                                                

然后,在Service层创建一个与查询结果匹配的DTO:

public class ProductCategorySummaryDto : Dto
{
    [Description("产品类别")]
    public string ProductCategory { get; set; }

    [Description("总数量")]
    public int TotalNum { get; set; }
}

在Service层的GetRawSQLResult()方法的定义:

public IList
                                                    
  
                                                    
                                                    
                                                    
                                                    
                                                    
                                                      GetRawSQLResult() { return IContractReviewProductRepository.GetRawSqlList 
                                                     
                                                       (); } 
                                                      
                                                    

9.2 另一方案:使用ExpandoObject对象返回查询结果
如果查询结果只使用一次,单独为它创建一个DTO 成本 似乎有些过高。下面同样是在Repository利用NHibernate执行原生SQL,但是返回值是一个动态对象的列表。

public IList
                                                       
  
                                                       
                                                       
                                                       
                                                       
                                                       
                                                         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
                                                         
  
                                                         
                                                         
                                                         
                                                         
                                                         
                                                           DynamicList(this IQuery query) { return query.SetResultTransformer(NhTransformers.ExpandoObject) .List 
                                                          
                                                            (); } } 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 
                                                           
                                                             )expando; for (int i = 0; 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
                                                           
  
                                                           
                                                           
                                                           
                                                           
                                                           
                                                             result = IContractReviewProductRepository().GetExpandoObjectList("5AB17F4D-803E-4641-8FCF-660662458BAA"); Assert.AreEqual("刮板机", result[0].DeviceNameText); Assert.AreEqual(4, result[0].Version); } 
                                                           

但是本质上ExpandoObject只是一个IDictionary。目前AutoMapper3.1还不支持把ExpandoObject对象映射成普通对象。没有编译期的语法检查,没有类型信息,没有静态的属性信息,将来想重构都十分不便。曾经非常羡慕Ruby等动态语言的灵活和便利,但是当C#向着动态语言大踏步前进时,反而有些感到害怕了。

转摘自:http://www.cnblogs.com/1-2-3/p/AutoMapper-Best-Practice.html


















































































































版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/208189.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 下午12:08
下一篇 2026年3月19日 下午12:09


相关推荐

  • goaccess 配置

    goaccess 配置log 11 22 111 5510 22 128 81POSTF54125 rest call 26 Nov 2019 15 14 13 0800 POST rest callHTTP 1 1 555goaccess conf time format Tdate format d b Ylog

    2026年3月19日
    2
  • go面试题整理(附带部分自己的解答)「建议收藏」

    go面试题整理(附带部分自己的解答)

    2022年2月16日
    53
  • stun协议笔记一(stun格式简介)「建议收藏」

    stun协议笔记一(stun格式简介)「建议收藏」一、stun协议格式1、STUN报文头1)最高的2位必须置零,这可以在当STUN和其他协议复用的时候,用来区分STUN包和其他数据包。2)STUNMessageType字段定义了消息的类型(请求/成功响应/失败响应/指示)和消息的主方法。虽然我们有4个消息类别,但在STUN中只有两种类型的事务,即请求/响应类型和指示类型。响应类型分为成功和出错两种,用来帮助快速处理STUN…

    2022年7月17日
    13
  • centos7配置国内yum源

    centos7配置国内yum源1、什么是yum仓库?yum仓库就是使用yum命令下载软件的镜像地址。我们通常使用yuminstall命令来在线安装linux系统的软件,这种方式可以自动处理依赖性关系,并且一次安装所有依赖的软体包,但是经常会遇到从国外镜像下载速度慢,无法下载的情况。那么此时我们就需要把我们的yum源改为国内的镜像。yum的配置文件yum的配置文件在/etc/yum.repos.d目录下…

    2022年6月6日
    40
  • 开曼群岛的中国大企业(Maluku_Islands)

    http://baike.baidu.com/view/29653.htm开曼群岛百科名片  开曼群岛地理位置开曼群岛(有时也译为凯门群岛)是英国在西加勒比群岛的一块海外属地,由大开曼、小开曼和开曼布拉克3个岛屿组成。开曼群岛是世界第四大离岸金融中心,并是著名的潜水胜地。 查看精彩图册

    2022年4月11日
    67
  • 02.pycharm中配置PyInstaller打包工具

    02.pycharm中配置PyInstaller打包工具我用的环境版本python解释器:3.6.6pycharm开发工具:2018.3.6社区版PyInstaller打包工具:4.5.1pycharm中配置PyInstaller打包工具opts可选的参数参数含义-F-onefile,打包成一个exe文件-D-onefile,创建一个目录,包含exe文件,但会依赖很多文件(默认选项)-c-console,-nowindowed,使用控制台,无窗口(默认)-w-Windowed,-noconsole,使用窗

    2025年7月7日
    6

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号