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();
            }
        }
    }
}

虽然AutoMapper并不强制要求在程序启动时一次性提供所有配置,但是这样做有如下好处:
a) 可以在程序启动时对所有的配置进行严格的验证(后文详述)。
b) 可以统一指定DTO向Entity映射时的通用行为(后文详述)。
c) 逻辑内聚:新增配置时方便模仿以前写过的配置;对项目中一共有多少DTO以及它们与实体的映射关系也容易有直观的把握。

  1. 在程序启动时对所有的配置进行严格的验证
    AutoMapper并不强制要求执行 Mapper.AssertConfigurationIsValid() 验证目标对象的所有属性都能找到源属性(或者在配置时指定了默认映射行为)。换句话说,即使执行 Mapper.AssertConfigurationIsValid() 验证失败了调用 Mapper() 也能成功映射(找不到源属性的目标属性将被赋默认值)。但是我们仍然应该在程序启动时对所有的配置进行严格的验证,并且在验证失败时立即找出原因并进行处理。因为我们在创建DTO时有可能因为手误造成DTO的属性与Entity的属性名称不完全一样;或者当Entity被重构,造成EntityDTO不完全匹配,这将造成许多隐性Bug,难以察觉,难以全部根除,这也是DTO经常被人诟病的一大缺点。使用AutoMapper的验证机制可以从根本上消除这一隐患,所以即使麻烦一点也要一直坚持进行验证。

  2. 指定DTO向Entity映射时的通用行为
    从DTO对象向Entity对象映射时,应该是先从数据库中加载Entity对象,然后把DTO对象的属性值覆盖到Entity对象中。Entity对象的IdVersion属性要么是从数据库中加载的(更新时),要么是由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()函数可以统一设置所有DTOEntity的映射都忽略IdVersion属性。

 /// <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>();
  1. 通过配置实现DTO向Entity映射时加载实体
    DTOEntity映射时,如果Entity有关联的属性,需要调用NHibernate的LoadEntity()根据Client传过来的关联属性Id加载实体对象。这项工作很适合放到AutoMapper的配置代码里。进一步地,我们可以约定:关联属性Idnull时,表示忽略此属性;如果关联属性Idstring.Empty,表示要把此属性置空;如果关联属性IdGUID,则加载实体对象。然后,把这个逻辑抽取出来形成 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))
  1. 让AutoMapper合并2个对象而不是创建新对象
    Map()方法有2种使用方式。一种是由AutoMapper创建目标对象:
ProductDto dto = Mapper.Map<Product, ProductDto>(entity);

另一种是让AutoMapper把源对象中的属性值合并/覆盖到目标对象:

ProductDto dto = new ProductDto();
Maper.Map(entity, dto);

应该总是使用后一种。对于EntityDTO映射的情况,由于有时候需要把2个Entity对象映射到一个DTO对象中,所以应该使用后一种方式。对于DTOEntity映射的情况,需要先从数据库中加载Entity对象,再把DTO对象中的部分属性值覆盖到Entity对象中。

  1. 考虑通过封装让AutoMapper可被取消和可替换

当我们使用外部工具的时候,一般总要想写办法尽量使这些工具容易被取消和替换,以避免技术风险,同时还能保证以更统一的方式使用工具。由于DTOEntity是不可见的,所以EntityDTO的映射和DTOEntity的映射方法都要添加到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.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);
    } 
}
  1. 使用扁平化的双向DTO

AutoMapper能够非常便利地根据命名约定生成扁平化的DTO。从DTOEntity映射时,需要配置根据属性Id加载实体的方法,在前文[4. 通过配置实现DTO向Entity映射时加载实体]有详细描述。

粒度过细的DTO不利于管理。一般一个扁平化的双向DTO就可以应付大多数场景了。扁平化的DTO不但可以让Client端得到更为简单的数据结构,节省流量,同时也是非常棒的解除循环引用的方案,方便Json序列化(后文详述)。

  1. 使用扁平化消除循环引用

AutoMapper在技术上是支持把带有循环引用的Entity对象映射为同样具有循环引用关系的DTO对象的。但是带有循环应用的DicCategoryDto对象在进一步Json序列化时,DicItemDtoCategory属性就会因为循环引用而被丢弃了。而像上图那样把多端扁平化,就可以仍然保留我们感兴趣的Category属性的信息了。

  1. 将DTO放置在Service

原则上Entity应该不知道DTO,所以物理上也最好把DTO放置在Service层里面。但是有一个技术问题:有时候需要在Repository层里面让NHibernate执行原生SQL语句,然后就需要利用NHibernateAliasToBean()方法将查询结果映射到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 = 0; i < tuple.Length; i++)
             {
                 string alias = aliases[i];
                 if (alias != null)
                 {
                     dictionary[alias] = tuple[i];
                 }
             }
             return expando;
         }
     }
 }

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#向着动态语言大踏步前进时,反而有些感到害怕了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
AutoMapper是一个用于对象映射的开源库。它可以帮助简化对象之间的转换过程,减少手动编写重复的代码。通过配置映射规则,AutoMapper可以自动将一个对象的属性值复制到另一个对象中对应的属性上,而不需要手动逐个属性进行赋值。 使用AutoMapper,你可以定义映射规则,包括源类型和目标类型以及它们之间的属性映射关系。一旦配置好映射规则,你可以使用简单的API将源对象映射到目标对象上。 以下是一个使用AutoMapper的示例: ```csharp // 定义源类型和目标类型 public class SourceObject { public string Name { get; set; } public int Age { get; set; } } public class DestinationObject { public string Name { get; set; } public int Age { get; set; } } // 配置映射规则 var config = new MapperConfiguration(cfg => { cfg.CreateMap<SourceObject, DestinationObject>(); }); // 创建映射器 var mapper = config.CreateMapper(); // 创建源对象 var source = new SourceObject { Name = "John", Age = 30 }; // 使用映射器进行对象映射 var destination = mapper.Map<SourceObject, DestinationObject>(source); // 输出目标对象属性值 Console.WriteLine(destination.Name); // 输出:John Console.WriteLine(destination.Age); // 输出:30 ``` 通过使用AutoMapper,你可以简化对象之间的映射过程,提高开发效率。它支持各种复杂的映射场景,并且可以通过自定义转换器来处理更复杂的映射逻辑。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值