重复造* SimpleMapper

时间:2022-09-15 21:23:37

  接手的项目还在用 TinyMapper 的一个早期版本用来做自动映射工具,TinyMapper 虽然速度快,但在配置里不能转换类型,比如 deleted 在数据库中用 0、1 表示,转换成实体模型时没法转换成 bool 类型,就为了这一个属性,就必须手写代码人工转换(怪不得有些 Mapper 作者认为 TinyMapper 是一个 toy)。
  于是试一试 AutoMapper,可是这货需要提前注册所有的映射关系,程序员本来就已经很累了。。。(最新版 TinyMapper 也要求提前注册所有映射关系)。
  找出以前使用过的 ValueInjecter,可扩展性很强,使用反射来实现。虽然我认为对现在处理器性能而言,快慢已经不太重要了,但它速度实在太慢了,有些测试项目消耗时间是 json反序列化的一半,TinyMapper 和 AutoMapper 均使用 emit 实现,非常接近手写代码的速度了。
  在 nuget.org 上找了找,还发现两个非常不错的 mapper:
  1. UltraMapper 不需要提前注册映射关系,而且使用 ReferenceTracking 解决了循环问题。
  2. HigLabo.Mapper 也不需要提前注册映射关系(看来牛人都对提前注册很不爽),支持 object 转换为Dictionary,提出了 PostAction 概念(自动映射出目标对象后,还可以执行自定义动作进行手工赋值,这样就不需要费力实现 Flattening 什么的了)。但试用过程中,发现不能实现 Array 到 List 的转换,而且作者也不打算改。。。

  既然各个 Mapper 都不太顺手,并且这段时间疫情封控,于是决定自己手撸一个POCO的 Mapper,目标如下:
  1. 不需要提前注册映射关系
  2. 像 json序列化/反序列化一样,同名属性尽可能映射(比如 int? 到 enum)
  3. 增加 HigLabo.Mapper的PostAction概念
  4. 使用 表达式树/Emit 提高速度

 

  编写过程中参考了 TinyMapper 和UltraMapper的代码,使用示例:

 1     public class Person
 2     {
 3         public int Id { get; set; }
 4 
 5         public string Name { get; set; }
 6         // Same Name, different type
 7         public byte? Age { get; set; }
 8     }
 9 
10     public class PersonDto
11     {
12         public int Id { get; set; }
13 
14         public string Name { get; set; }
15 
16         public int? Age { get; set; }
17 
18         public string Description { get; set; }
19     }
20 
21     class Program
22     {
23         static void Main(string[] args)
24         {
25             // register PostAction
26             ZK.Mapper.SimpleMapper.Default.SetPostAction<Person, PersonDto>((p, dto) =>
27             {
28                 if (dto == null)
29                 {
30                     return dto;
31                 }
32                 dto.Description = p.Age.HasValue ? $"{p.Name} age is {p.Age}" : $"{p.Name} age is unknown";
33                 return dto;
34             });
35 
36             var p = new Person()
37             {
38                 Id = 1,
39                 Name = "john",
40                 Age = 32
41             };
42 
43             var dto = ZK.Mapper.SimpleMapper.Default.Map<PersonDto>(p);
44             Console.WriteLine(dto.Description);
45         }
46     }

 

  写写这里面的总结吧
  1. 内部Mapper都是泛型的,但使用时传入的source很可能是 object,所以都是使用 反射创建泛型化的Mapper实例,然后建立TypePair的对应关系,这样就解偶了泛型
  2. Emit 和 表达式树原理都是一样的,建立IL代码,所以效率非常接近
  3. 如果能像 AutoMapper 那样提前注册所有映射关系,速度优化的手段会更多,估计这也是 TinyMapper 转成提前注册的原因吧。很多 Mapper 的性能测试都号称比 AutoMapper 快,但引用的都是老版本的 AutoMapper,但现在 AutoMapper 非常快,在一些简单测试里赶上了 TinyMapper。如果添加了很多特色功能,却很拖累速度。当然我还是觉得只要不是数量级的差距,都不太重要。
  4. 我的潜意识里 SimpleMapper 就为解决当前项目的问题,比如从数据库中读出来对象,映射成Dto后,就不会被再使用了,所以SimpleMapper默认是浅拷贝。所以我也不打算发布到nuget。
  5. SimpleMapper 功能不多,也没有为性能做太多优化,所以结构比较清晰,有兴趣的朋友阅读起来应该不难。

 

  SimpleMapper 代码地址