婶可忍叔不可忍的AutoMapper,你还用吗?

AutoMapper是让人又爱又恨的项目

  • 爱它是因为它解决了一些问题,很多项目都有用,下载量很大,受众很广。
  • 恨它是因为它诸多反人类的设计。
  • 为此本人开源项目PocoEmit对标AutoMapper。

1. AutoMapper反人类设计

1.1 AutoMapper注册代码

services.AddAutoMapper(cfg => cfg.CreateMap<User, UserDTO>()); 

User和UserDTO除了类名不一样,其他都一样,怎么看这行代码都多余。
需要转化的类型越多,多余的代码就越多。
类型转化不应该就是个静态方法吗?而且AutoMapper注册却依赖容器,Mapper对象也是从容器获取。
本人觉得AutoMapper设计的太反人类了。

1.2 PocoEmit对于大部分转化是不需要手动配置

  • PocoEmit可以轻松的定义静态实例。
  • PocoEmit静态实例可以用来定义静态委托字段,当静态方法用。
UserDTO dto = PocoEmit.Mapper.Default.Convert<User, UserDTO>(new User()); 
public static readonly Func<User, UserDTO> UserDTOConvert = PocoEmit.Mapper.Default.GetConvertFunc<User, UserDTO>(); 

2. AutoMapper的性能差强人意

2.1 以下是AutoMapper官网例子与PocoEmit.Mapper的对比

  • Customer转化为CustomerDTO(嵌套多个子对象、数组及列表)。
  • Auto是执行AutoMapper的IMapper.Map方法。
  • Poco是执行PocoEmit.Mapper的IMapper.Convert方法。
  • PocoFunc是执行PocoEmit.Mapper生成的委托。
Method Mean Error StdDev Median Ratio RatioSD Gen0 Allocated Alloc Ratio
Auto 89.30 ns 1.006 ns 1.118 ns 90.17 ns 1.46 0.03 0.0260 448 B 1.08
Poco 61.31 ns 1.036 ns 1.194 ns 61.25 ns 1.00 0.03 0.0241 416 B 1.00
PocoFunc 42.56 ns 0.066 ns 0.073 ns 42.56 ns 0.69 0.01 0.0223 384 B 0.92
  • Auto耗时比Poco多50%左右。
  • Auto耗时是PocoFunc的两倍多。

2.2 能不能用AutoMapper生成委托来提高性能呢

  • 既可以说能也可以说不能。
  • 说能是因为AutoMapper确实提供了该功能。
  • 说不能是因为AutoMapper没打算给用户用。

2.2.1 AutoMapper生成委托有点麻烦

var configuration = _auto.ConfigurationProvider.Internal(); var mapRequest = new MapRequest(new TypePair(typeof(Customer), typeof(CustomerDTO))); Func<Customer, CustomerDTO, ResolutionContext, CustomerDTO> autoFunc = configuration.GetExecutionPlan<Customer, CustomerDTO>(mapRequest); 

作为对比PocoEmit.Mapper就简单的多了

Func<Customer, CustomerDTO> pocoFunc = PocoEmit.Mapper.Default.GetConvertFunc<Customer, CustomerDTO>(); 

2.2.2 调用AutoMapper生成的委托更麻烦

  • 参数ResolutionContext没有公开的构造函数,也找不到公开的实例。
  • 只能通过反射获得ResolutionContext的实例。
var field = typeof(AutoMapper.Mapper).GetField("_defaultContext", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); ResolutionContext resolutionContext = field.GetValue(_auto) as ResolutionContext; 

2.2.3 加入AutoMapper生成委托再对比一下

Method Mean Error StdDev Median Ratio RatioSD Gen0 Allocated Alloc Ratio
Auto 89.30 ns 1.006 ns 1.118 ns 90.17 ns 1.46 0.03 0.0260 448 B 1.08
AutoFunc 56.04 ns 0.103 ns 0.119 ns 56.03 ns 0.91 0.02 0.0260 448 B 1.08
Poco 61.31 ns 1.036 ns 1.194 ns 61.25 ns 1.00 0.03 0.0241 416 B 1.00
PocoFunc 42.56 ns 0.066 ns 0.073 ns 42.56 ns 0.69 0.01 0.0223 384 B 0.92
  • AutoMapper生成委托确实也快了不少。
  • 从百分比来看即使不生成委托,AutoMapper也慢不了多少?没有数量级的区别,能忍? --- 反问句

2.3 简单类型转化对比

  • User转UserDTO,只有两个简单属性
Method Mean Error StdDev Ratio Gen0 Allocated Alloc Ratio
Auto 35.436 ns 0.0455 ns 0.0505 ns 1.57 0.0019 32 B 0.50
AutoFunc 4.159 ns 0.0847 ns 0.0906 ns 0.18 0.0019 32 B 0.50
Poco 22.607 ns 0.1754 ns 0.1801 ns 1.00 0.0037 64 B 1.00
PocoFunc 3.818 ns 0.0176 ns 0.0180 ns 0.17 0.0019 32 B 0.50
  • Auto耗时是AutoFunc差不多十倍,差出一个数量级了(回答了前面的反问)
  • AutoFunc耗时比PocoFunc稍多,这说明AutoMapper复杂类型转化性能非常不好,简单类型转化可能还能凑合
  • 关键是性能好生成的委托AutoMapper不给用啊,“婶可忍叔不可忍”啊!

3. AutoMapper生成的代码能通过代码审核吗?

3.1 AutoMapper官网那个例子生成以下代码

T __f<T>(System.Func<T> f) => f(); CustomerDTO _autoMap(Customer source, CustomerDTO destination, ResolutionContext context) {     return (source == null) ?         (destination == null) ? (CustomerDTO)null : destination :         __f(() => {             CustomerDTO typeMapDestination = null;             typeMapDestination = destination ?? new CustomerDTO();             try             {                 typeMapDestination.Id = source.Id;             }             catch (Exception ex)             {                 throw TypeMapPlanBuilder.MemberMappingError(                     ex,                     default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);             }             try             {                 typeMapDestination.Name = source.Name;             }             catch (Exception ex)             {                 throw TypeMapPlanBuilder.MemberMappingError(                     ex,                     default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);             }             try             {                 Address resolvedValue = null;                 Address mappedValue = null;                 resolvedValue = source.Address;                 mappedValue = (resolvedValue == null) ? (Address)null :                     ((Func<Address, Address, ResolutionContext, Address>)((                         Address source_1,                         Address destination_1,                         ResolutionContext context) => //Address                         (source_1 == null) ?                             (destination_1 == null) ? (Address)null : destination_1 :                             __f(() => {                                 Address typeMapDestination_1 = null;                                 typeMapDestination_1 = destination_1 ?? new Address();                                 try                                 {                                     typeMapDestination_1.Id = source_1.Id;                                 }                                 catch (Exception ex)                                 {                                     throw TypeMapPlanBuilder.MemberMappingError(                                         ex,                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                 }                                 try                                 {                                     typeMapDestination_1.Street = source_1.Street;                                 }                                 catch (Exception ex)                                 {                                     throw TypeMapPlanBuilder.MemberMappingError(                                         ex,                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                 }                                 try                                 {                                     typeMapDestination_1.City = source_1.City;                                 }                                 catch (Exception ex)                                 {                                     throw TypeMapPlanBuilder.MemberMappingError(                                         ex,                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                 }                                 try                                 {                                     typeMapDestination_1.Country = source_1.Country;                                 }                                 catch (Exception ex)                                 {                                     throw TypeMapPlanBuilder.MemberMappingError(                                         ex,                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                 }                                 return typeMapDestination_1;                             })))                     .Invoke(                         resolvedValue,                         (destination == null) ? (Address)null :                             typeMapDestination.Address,                         context);                 typeMapDestination.Address = mappedValue;             }             catch (Exception ex)             {                 throw TypeMapPlanBuilder.MemberMappingError(                     ex,                     default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);             }             try             {                 Address resolvedValue_1 = null;                 AddressDTO mappedValue_1 = null;                 resolvedValue_1 = source.HomeAddress;                 mappedValue_1 = (resolvedValue_1 == null) ? (AddressDTO)null :                     ((Func<Address, AddressDTO, ResolutionContext, AddressDTO>)((                         Address source_2,                         AddressDTO destination_2,                         ResolutionContext context) => //AddressDTO                         (source_2 == null) ?                             (destination_2 == null) ? (AddressDTO)null : destination_2 :                             __f(() => {                                 AddressDTO typeMapDestination_2 = null;                                 typeMapDestination_2 = destination_2 ?? new AddressDTO();                                 try                                 {                                     typeMapDestination_2.Id = source_2.Id;                                 }                                 catch (Exception ex)                                 {                                     throw TypeMapPlanBuilder.MemberMappingError(                                         ex,                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                 }                                 try                                 {                                     typeMapDestination_2.City = source_2.City;                                 }                                 catch (Exception ex)                                 {                                     throw TypeMapPlanBuilder.MemberMappingError(                                         ex,                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                 }                                 try                                 {                                     typeMapDestination_2.Country = source_2.Country;                                 }                                 catch (Exception ex)                                 {                                     throw TypeMapPlanBuilder.MemberMappingError(                                         ex,                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                 }                                 return typeMapDestination_2;                             })))                     .Invoke(                         resolvedValue_1,                         (destination == null) ? (AddressDTO)null :                             typeMapDestination.HomeAddress,                         context);                 typeMapDestination.HomeAddress = mappedValue_1;             }             catch (Exception ex)             {                 throw TypeMapPlanBuilder.MemberMappingError(                     ex,                     default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);             }             try             {                 Address[] resolvedValue_2 = null;                 AddressDTO[] mappedValue_2 = null;                 resolvedValue_2 = source.Addresses;                 mappedValue_2 = (resolvedValue_2 == null) ?                     Array.Empty<AddressDTO>() :                     __f(() => {                         AddressDTO[] destinationArray = null;                         int destinationArrayIndex = default;                         destinationArray = new AddressDTO[resolvedValue_2.Length];                         destinationArrayIndex = default(int);                         int sourceArrayIndex = default;                         Address sourceItem = null;                         sourceArrayIndex = default(int);                         while (true)                         {                             if ((sourceArrayIndex < resolvedValue_2.Length))                             {                                 sourceItem = resolvedValue_2[sourceArrayIndex];                                 destinationArray[destinationArrayIndex++] = ((Func<Address, AddressDTO, ResolutionContext, AddressDTO>)((                                     Address source_2,                                     AddressDTO destination_2,                                     ResolutionContext context) => //AddressDTO                                     (source_2 == null) ?                                         (destination_2 == null) ? (AddressDTO)null : destination_2 :                                         __f(() => {                                             AddressDTO typeMapDestination_2 = null;                                             typeMapDestination_2 = destination_2 ?? new AddressDTO();                                             try                                             {                                                 typeMapDestination_2.Id = source_2.Id;                                             }                                             catch (Exception ex)                                             {                                                 throw TypeMapPlanBuilder.MemberMappingError(                                                     ex,                                                     default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                             }                                             try                                             {                                                 typeMapDestination_2.City = source_2.City;                                             }                                             catch (Exception ex)                                             {                                                 throw TypeMapPlanBuilder.MemberMappingError(                                                     ex,                                                     default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                             }                                             try                                             {                                                 typeMapDestination_2.Country = source_2.Country;                                             }                                             catch (Exception ex)                                             {                                                 throw TypeMapPlanBuilder.MemberMappingError(                                                     ex,                                                     default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                             }                                             return typeMapDestination_2;                                         })))                                 .Invoke(                                     sourceItem,                                     (AddressDTO)null,                                     context);                                 sourceArrayIndex++;                             }                             else                             {                                 goto LoopBreak;                             }                         }                     LoopBreak:;                         return destinationArray;                     });                 typeMapDestination.Addresses = mappedValue_2;             }             catch (Exception ex)             {                 throw TypeMapPlanBuilder.MemberMappingError(                     ex,                     default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);             }             try             {                 List<Address> resolvedValue_3 = null;                 List<AddressDTO> mappedValue_3 = null;                 resolvedValue_3 = source.WorkAddresses;                 mappedValue_3 = (resolvedValue_3 == null) ?                     new List<AddressDTO>() :                     __f(() => {                         List<AddressDTO> collectionDestination = null;                         List<AddressDTO> passedDestination = null;                         passedDestination = (destination == null) ? (List<AddressDTO>)null :                             typeMapDestination.WorkAddresses;                         collectionDestination = passedDestination ?? new List<AddressDTO>();                         collectionDestination.Clear();                         List<Address>.Enumerator enumerator = default;                         Address item = null;                         enumerator = resolvedValue_3.GetEnumerator();                         try                         {                             while (true)                             {                                 if (enumerator.MoveNext())                                 {                                     item = enumerator.Current;                                     collectionDestination.Add(((Func<Address, AddressDTO, ResolutionContext, AddressDTO>)((                                         Address source_2,                                         AddressDTO destination_2,                                         ResolutionContext context) => //AddressDTO                                         (source_2 == null) ?                                             (destination_2 == null) ? (AddressDTO)null : destination_2 :                                             __f(() => {                                                 AddressDTO typeMapDestination_2 = null;                                                 typeMapDestination_2 = destination_2 ?? new AddressDTO();                                                 try                                                 {                                                     typeMapDestination_2.Id = source_2.Id;                                                 }                                                 catch (Exception ex)                                                 {                                                     throw TypeMapPlanBuilder.MemberMappingError(                                                         ex,                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                                 }                                                 try                                                 {                                                     typeMapDestination_2.City = source_2.City;                                                 }                                                 catch (Exception ex)                                                 {                                                     throw TypeMapPlanBuilder.MemberMappingError(                                                         ex,                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                                 }                                                 try                                                 {                                                     typeMapDestination_2.Country = source_2.Country;                                                 }                                                 catch (Exception ex)                                                 {                                                     throw TypeMapPlanBuilder.MemberMappingError(                                                         ex,                                                         default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);                                                 }                                                 return typeMapDestination_2;                                             })))                                     .Invoke(                                         item,                                         (AddressDTO)null,                                         context));                                 }                                 else                                 {                                     goto LoopBreak_1;                                 }                             }                         LoopBreak_1:;                         }                         finally                         {                             enumerator.Dispose();                         }                         return collectionDestination;                     });                 typeMapDestination.WorkAddresses = mappedValue_3;             }             catch (Exception ex)             {                 throw TypeMapPlanBuilder.MemberMappingError(                     ex,                     default(PropertyMap)/*NOTE: Provide the non-default value for the Constant!*/);             }             return typeMapDestination;         }); }     

3.2 以下是PocoEmit.Mapper生成的代码

T __f<T>(System.Func<T> f) => f(); CustomerDTO _pocoConvert(Customer source) {     CustomerDTO dest = null;     return (source == (Customer)null) ? (CustomerDTO)null :         __f(() => {             dest = new CustomerDTO();             int member0 = default;             string member1 = null;             Address member2 = null;             Address member3 = null;             Address[] member4 = null;             List<Address> member5 = null;             member0 = source.Id;             dest.Id = member0;             member1 = source.Name;             dest.Name = member1;             member2 = source.Address;             if ((member2 != null))             {                 dest.Address = member2;             }             member3 = source.HomeAddress;             if ((member3 != null))             {                 // { The block result will be assigned to `dest.HomeAddress`                 AddressDTO dest_1 = null;                 dest.HomeAddress = (member3 == (Address)null) ? (AddressDTO)null :                     __f(() => {                         dest_1 = new AddressDTO();                         int member0_1 = default;                         string member1_1 = null;                         string member2_1 = null;                         member0_1 = member3.Id;                         dest_1.Id = member0_1;                         member1_1 = member3.City;                         dest_1.City = member1_1;                         member2_1 = member3.Country;                         dest_1.Country = member2_1;                         return dest_1;                     });                 // } end of block assignment;             }             member4 = source.Addresses;             if ((member4 != null))             {                 // { The block result will be assigned to `dest.Addresses`                 int count = default;                 AddressDTO[] dest_2 = null;                 int index = default;                 Address sourceItem = null;                 count = member4.Length;                 dest_2 = new AddressDTO[count];                 index = 0;                 while (true)                 {                     if ((index < count))                     {                         sourceItem = member4[index];                         // { The block result will be assigned to `dest_2[index]`                         AddressDTO dest_3 = null;                         dest_2[index] = (sourceItem == (Address)null) ? (AddressDTO)null :                             __f(() => {                                 dest_3 = new AddressDTO();                                 int member0_2 = default;                                 string member1_2 = null;                                 string member2_2 = null;                                 member0_2 = sourceItem.Id;                                 dest_3.Id = member0_2;                                 member1_2 = sourceItem.City;                                 dest_3.City = member1_2;                                 member2_2 = sourceItem.Country;                                 dest_3.Country = member2_2;                                 return dest_3;                             });                         // } end of block assignment                         index++;                     }                     else                     {                         goto forLabel;                     }                 }             forLabel:;                 dest.Addresses = dest_2;                 // } end of block assignment;             }             member5 = source.WorkAddresses;             if ((member5 != null))             {                 // { The block result will be assigned to `dest.WorkAddresses`                 List<AddressDTO> dest_4 = null;                 dest_4 = new List<AddressDTO>(member5.Count);                 int index_1 = default;                 int len = default;                 index_1 = 0;                 len = member5.Count;                 while (true)                 {                     if ((index_1 < len))                     {                         Address sourceItem_1 = null;                         AddressDTO destItem = null;                         sourceItem_1 = member5[index_1];                         // { The block result will be assigned to `destItem`                         AddressDTO dest_5 = null;                         destItem = (sourceItem_1 == (Address)null) ? (AddressDTO)null :                         __f(() => {                                     dest_5 = new AddressDTO();                                     int member0_3 = default;                                     string member1_3 = null;                                     string member2_3 = null;                                     member0_3 = sourceItem_1.Id;                                     dest_5.Id = member0_3;                                     member1_3 = sourceItem_1.City;                                     dest_5.City = member1_3;                                     member2_3 = sourceItem_1.Country;                                     dest_5.Country = member2_3;                                     return dest_5;                                 });                         // } end of block assignment;                         dest_4.Add(destItem);                         index_1++;                     }                     else                     {                         goto forLabel_1;                     }                 }             forLabel_1:;                 dest.WorkAddresses = dest_4;                 // } end of block assignment;             }             CustomerConvertBench.ConvertAddressCity(                 source,                 dest);             return dest;         }); } 

3.3 简单对比如下

  • AutoMapper生成代码三百多行,PocoEmit.Mapper一百多行,AutoMapper代码量是两倍以上
  • AutoMapper生成大量try catch,哪怕是int对int赋值也要try
  • AutoMapper用迭代器Enumerator访问列表,PocoEmit.Mapper用索引器
  • AutoMapper这些区别应该是导致性能差的部分原因

3.4 如何获取AutoMapper生成的代码

LambdaExpression expression = _auto.ConfigurationProvider.BuildExecutionPlan(typeof(Customer), typeof(CustomerDTO));    

3.4.1 如果要查看更可读的代码推荐使用FastExpressionCompiler

  • 可以使用nuget安装
  • 前面的例子就是使用FastExpressionCompiler再手动整理了一下
string code = FastExpressionCompiler.ToCSharpPrinter.ToCSharpString(expression); 

3.4.2 PocoEmit获取生成代码更简单

Expression<Func<Customer, CustomerDTO>> expression =  PocoEmit.Mapper.Default.BuildConverter<Customer, CustomerDTO>(); string code = FastExpressionCompiler.ToCSharpPrinter.ToCSharpString(expression); 

3.4.3 PocoEmit生成代码扩展性

  • PocoEmit可以获取委托表达式自己来编译委托
  • PocoEmit通过PocoEmit.Builders.Compiler.Instance来编译,可以对Instance进行覆盖来扩展
  • 通过实现Compiler类,只需要重写CompileFunc和CompileAction两个方法
  • 可以使用FastExpressionCompiler来实现Compiler类

4. AutoMapper枚举逻辑问题

public enum MyColor {     None = 0,     Red = 1,     Green = 2,     Blue = 3, } ConsoleColor color = ConsoleColor.DarkBlue; // Red MyColor autoColor = _auto.Map<ConsoleColor, MyColor>(color); // None MyColor pocoColor = PocoEmit.Mapper.Default.Convert<ConsoleColor, MyColor>(color); 
  • AutoMapper先按枚举名转化,失败再按值转化,不支持的DarkBlue被AutoMapper转化为Red
  • 不同类型的枚举值转化没有意义,定义枚举可以不指定值
  • AutoMapper这完全是犯了画蛇添足的错误
  • AutoMapper还有哪些槽点欢迎大家在评论区指出

5. PocoEmit可扩展架构

5.1 nuget安装PocoEmit可获得基础功能

  • 通过PocoEmit可以读写实体的属性
  • PocoEmit可以通过PocoEmit.Poco转化基础类型和枚举
  • PocoEmit.Poco支持注册转化表达式

5.2 nuget安装PocoEmit.Mapper获得更多功能

  • PocoEmit.Mapper可以支持PocoEmit.Poco的所有功能
  • PocoEmit.Mapper可以支持自定义实体类型(不支持集合(含数组、列表及字典)成员)的转化和复制

5.3 nuget安装PocoEmit.Collections扩展集合功能

  • 通过UseCollection扩展方法给PocoEmit.Mapper增加集合功能
  • 扩展后PocoEmit.Mapper支持集合(含数组、列表及字典)的转化和复制
  • 支持实体类型包含集合成员的转化和复制
  • 嫌麻烦的同学可以直接安装PocoEmit.Collections并配置UseCollection

源码托管地址: https://github.com/donetsoftwork/MyEmit ,也欢迎大家直接查看源码。
gitee同步更新:https://gitee.com/donetsoftwork/MyEmit

如果大家喜欢请动动您发财的小手手帮忙点一下Star。

发表评论

评论已关闭。

相关文章