zoukankan      html  css  js  c++  java
  • 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全局配置代码,里面的一些细节会在后面逐一解释。

     1 public class DtoMapping
     2 {
     3     private readonly IContractReviewMainAppServices IContractReviewMainAppServices;
     4     private readonly IDictionaryAppService IDictionaryAppService;
     5     private readonly IProductAppService IProductAppService;
     6     public DtoMapping(IContractReviewMainAppServices IContractReviewMainAppServices,
     7           IDictionaryAppService IDictionaryAppService, IProductAppService IProductAppService)
     8     {
     9         this.IContractReviewMainAppServices = IContractReviewMainAppServices;
    10         this.IDictionaryAppService = IDictionaryAppService;
    11         this.IProductAppService = IProductAppService;
    12     }
    13 
    14     public void InitMapping()
    15     {
    16         #region 合同购买设备信息
    17         Mapper.CreateMap<ContractReviewProduct, ContractReviewProductDto>();
    18         Mapper.CreateMap<ContractReviewProductDto, ContractReviewProduct>() // DTO 向 Entity 赋值
    19               .ForMember(entity => entity.ContractReviewMain, opt => LoadEntity(opt,
    20                                                                                 dto => dto.ContractReviewMainId,
    21                                                                                 IContractReviewMainAppServices.Get))
    22               .ForMember(entity => entity.DeviceCategory, opt => LoadEntity(opt,
    23                                                                             dto => dto.DeviceCategoryId,
    24                                                                             IDictionaryAppService.FindDicItem))
    25               .ForMember(entity => entity.DeviceName, opt => LoadEntity(opt,
    26                                                                         dto => dto.DeviceNameId,
    27                                                                         IProductAppService.FindProduct))
    28               .ForMember(entity => entity.ProductModel, opt => LoadEntity(opt,
    29                                                                           dto => dto.ProductModelId,
    30                                                                           IProductAppService.FindProduct))
    31               .ForMember(entity => entity.Unit, opt => LoadEntity(opt,
    32                                                                   dto => dto.UnitId,
    33                                                                   IDictionaryAppService.FindDicItem))
    34               .ForMember(entity => entity.Creator, opt => opt.Ignore()); // DTO 里面没有的属性直接Ignore
    35         #endregion 合同购买设备信息
    36 
    37         #region 字典配置
    38         Mapper.CreateMap<DicCategory, DicCategoryDto>();
    39         Mapper.CreateMap<DicCategoryDto, DicCategory>();
    40         Mapper.CreateMap<DicItem, DicItemDto>();
    41         Mapper.CreateMap<DicItemDto, DicItem>()
    42               .ForMember(entity => entity.Category, opt => LoadEntity(opt,
    43                                                                       dto => dto.CategoryId,
    44                                                                       IDictionaryAppService.FindDicCategory));
    45         #endregion 字典配置
    46 
    47         // 对于所有的 DTO 到 Entity 的映射,都忽略 Id 和 Version 属性
    48         IgnoreDtoIdAndVersionPropertyToEntity();
    49 
    50         // 验证配置
    51         Mapper.AssertConfigurationIsValid();
    52     }
    53 
    54     /// <summary>
    55     /// 加载实体对象。
    56     /// <remarks>Id是null的会被忽略;Id是string.Empty的将被赋值为null;Id是GUID的将从数据库中加载并赋值。</remarks> 
    57     /// </summary>
    58     /// <typeparam name="TSource"></typeparam>
    59     /// <typeparam name="TMember"></typeparam>
    60     /// <param name="opt"></param>
    61     /// <param name="getId"></param>
    62     /// <param name="doLoad"></param>
    63     private void LoadEntity<TSource, TMember>(IMemberConfigurationExpression<TSource> opt,
    64         Func<TSource, string> getId, Func<string, TMember> doLoad) where TMember : class
    65     {
    66         opt.Condition(src => (getId(src) != null));
    67         opt.MapFrom(src => getId(src) == string.Empty ? null : doLoad(getId(src)));
    68     }
    69 
    70     /// <summary>
    71     /// 对于所有的 DTO 到 Entity 的映射,都忽略 Id 和 Version 属性
    72     /// <remarks>当从DTO向Entity赋值时,要保持从数据库中加载过来的Entity的Id和Version属性不变!</remarks>
    73     /// </summary>
    74     private void IgnoreDtoIdAndVersionPropertyToEntity()
    75     {
    76         PropertyInfo idProperty = typeof(Entity).GetProperty("Id");
    77         PropertyInfo versionProperty = typeof(Entity).GetProperty("Version");
    78         foreach (TypeMap map in Mapper.GetAllTypeMaps())
    79         {
    80             if (typeof(Dto).IsAssignableFrom(map.SourceType)
    81                 && typeof(Entity).IsAssignableFrom(map.DestinationType))
    82             {
    83                 map.FindOrCreatePropertyMapFor(new PropertyAccessor(idProperty)).Ignore();
    84                 map.FindOrCreatePropertyMapFor(new PropertyAccessor(versionProperty)).Ignore();
    85             }
    86         }
    87     }
    88 }
    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。

     1 /// <summary>
     2 /// 数据传输对象抽象类
     3 /// </summary>
     4 public abstract class Dto
     5 {
     6     /// <summary>
     7     /// 从实体中取得属性值
     8     /// </summary>
     9     /// <param name="entity"></param>
    10     public virtual void FetchValuesFromEntity<TEntity>(TEntity entity)
    11     {
    12         Mapper.Map(entity, this, entity.GetType(), this.GetType());
    13     }
    14 
    15     /// <summary>
    16     /// 将DTO中的属性值赋值到实体对象中
    17     /// </summary>
    18     /// <param name="entity"></param>
    19     public virtual void AssignValuesToEntity<TEntity>(TEntity entity)
    20     {
    21         Mapper.Map(this, entity, this.GetType(), entity.GetType());
    22     }
    23 
    24     [Description("主键Id")]
    25     public string Id { get; set; }
    26 
    27     [Description("版本号")]
    28     public int Version { get; set; }
    29 }
    30 
    31 /// <summary>
    32 /// 业务DTO基类
    33 /// </summary>
    34 public abstract class BizInfoDto : Dto
    35 {
    36     [Description("删除标识")]
    37     public bool Del { get; set; }
    38 
    39     [Description("最后更新时间")]
    40     public DateTime? UpdateTime { get; set; }
    41 
    42     [Description("数据产生时间")]
    43     public DateTime? CreateTime { get; set; }
    44 }
    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()方法是一个自定义的扩展方法:

     1 public static class NHibernateExtensions
     2 {
     3     public static IList<dynamic> DynamicList(this IQuery query)
     4     {
     5         return query.SetResultTransformer(NhTransformers.ExpandoObject)
     6                     .List<dynamic>();
     7     }
     8 }
     9 
    10 public static class NhTransformers
    11 {
    12     public static readonly IResultTransformer ExpandoObject;
    13 
    14     static NhTransformers()
    15     {
    16         ExpandoObject = new ExpandoObjectResultSetTransformer();
    17     }
    18 
    19     private class ExpandoObjectResultSetTransformer : IResultTransformer
    20     {
    21         public IList TransformList(IList collection)
    22         {
    23             return collection;
    24         }
    25 
    26         public object TransformTuple(object[] tuple, string[] aliases)
    27         {
    28             var expando = new ExpandoObject();
    29             var dictionary = (IDictionary<string, object>)expando;
    30             for (int i = 0; i < tuple.Length; i++)
    31             {
    32                 string alias = aliases[i];
    33                 if (alias != null)
    34                 {
    35                     dictionary[alias] = tuple[i];
    36                 }
    37             }
    38             return expando;
    39         }
    40     }
    41 }
    DynamicList()扩展方法和ExpandoObjectResultSetTransformer

    在Service层使用返回的动态对象的代码与使用普通代码看上去一样。也可以直接把返回的动态对象利用Json.Net序列化。

    [TestMethod]
    public void TestGetExpandoObject()
    {
        IList<dynamic> 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#向着动态语言大踏步前进时,反而有些感到害怕了。

  • 相关阅读:
    Binary Tree Maximum Path Sum
    ZigZag Conversion
    Longest Common Prefix
    Reverse Linked List II
    Populating Next Right Pointers in Each Node
    Populating Next Right Pointers in Each Node II
    Rotate List
    Path Sum II
    [Leetcode]-- Gray Code
    Subsets II
  • 原文地址:https://www.cnblogs.com/1-2-3/p/AutoMapper-Best-Practice.html
Copyright © 2011-2022 走看看