AutoMapper是让人又爱又恨的项目
- 爱它是因为它解决了一些问题,很多项目都有用,下载量很大,受众很广。
- 恨它是因为它诸多反人类的设计。
- 为此本人开源项目PocoEmit对标AutoMapper。
1. AutoMapper反人类设计
1.1 AutoMapper注册代码
- services.AddAutoMapper(cfg => cfg.CreateMap<User, UserDTO>());
复制代码 User和UserDTO除了类名不一样,其他都一样,怎么看这行代码都多余。
需要转化的类型越多,多余的代码就越多。
类型转化不应该就是个静态方法吗?而且AutoMapper注册却依赖微软容器,
本人觉得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生成的委托
MethodMeanErrorStdDevMedianRatioRatioSDGen0AllocatedAlloc RatioAuto89.30 ns1.006 ns1.118 ns90.17 ns1.460.030.0260448 B1.08Poco61.31 ns1.036 ns1.194 ns61.25 ns1.000.030.0241416 B1.00PocoFunc42.56 ns0.066 ns0.073 ns42.56 ns0.690.010.0223384 B0.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生成委托再对比一下
MethodMeanErrorStdDevMedianRatioRatioSDGen0AllocatedAlloc RatioAuto89.30 ns1.006 ns1.118 ns90.17 ns1.460.030.0260448 B1.08AutoFunc56.04 ns0.103 ns0.119 ns56.03 ns0.910.020.0260448 B1.08Poco61.31 ns1.036 ns1.194 ns61.25 ns1.000.030.0241416 B1.00PocoFunc42.56 ns0.066 ns0.073 ns42.56 ns0.690.010.0223384 B0.92
- AutoMapper生成委托确实也快了不少
- 从百分比来看即使不生成委托,AutoMapper也慢不了多少?没有数量级的区别,能忍? --- 反问句
2.3 简单类型转化对比
MethodMeanErrorStdDevRatioGen0AllocatedAlloc RatioAuto35.436 ns0.0455 ns0.0505 ns1.570.001932 B0.50AutoFunc4.159 ns0.0847 ns0.0906 ns0.180.001932 B0.50Poco22.607 ns0.1754 ns0.1801 ns1.000.003764 B1.00PocoFunc3.818 ns0.0176 ns0.0180 ns0.170.001932 B0.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 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 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() :
- __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 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 resolvedValue_3 = null;
- List mappedValue_3 = null;
- resolvedValue_3 = source.WorkAddresses;
- mappedValue_3 = (resolvedValue_3 == null) ?
- new List() :
- __f(() => {
- List collectionDestination = null;
- List passedDestination = null;
- passedDestination = (destination == null) ? (List)null :
- typeMapDestination.WorkAddresses;
- collectionDestination = passedDestination ?? new List();
- collectionDestination.Clear();
- List.Enumerator enumerator = default;
- Address item = null;
- enumerator = resolvedValue_3.GetEnumerator();
- try
- {
- while (true)
- {
- if (enumerator.MoveNext())
- {
- item = enumerator.Current;
- collectionDestination.Add(((Func)((
- 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生成的代码
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。
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除 |