明志唯新

可能是 .NET 领域性能最好的对象映射框架 —— Mapster

发表于

我之前文章提到过 MediatR 的作者 Jimmy Bogard,他也是大名鼎鼎的对象映射框架 AutoMapper 的作者。AutoMapper 的功能强大,在 .NET 领域的开发者中有非常高的知名度和使用率。而今天老衣要提的是另外一款高性能对象映射框架:Mapster。它轻巧便捷,功能也非常强大,关键是性能很高——有可能是 .NET 领域性能最好的。

我们先来看看性能

与 AutoMapper 相比,Mapster 在速度和内存占用方面表现更加优秀,下面是官方给出的稍早版本 6.0 的性能对比表:

Method Mean StdDev Error Gen 0 Gen 1 Gen 2 Allocated
'Mapster 6.0.0' 108.59 ms 1.198 ms 1.811 ms 31000.0000 - - 124.36 MB
'Mapster 6.0.0 (Roslyn)' 38.45 ms 0.494 ms 0.830 ms 31142.8571 - - 124.36 MB
'Mapster 6.0.0 (FEC)' 37.03 ms 0.281 ms 0.472 ms 29642.8571 - - 118.26 MB
'Mapster 6.0.0 (Codegen)' 34.16 ms 0.209 ms 0.316 ms 31133.3333 - - 124.36 MB
'ExpressMapper 1.9.1' 205.78 ms 5.357 ms 8.098 ms 59000.0000 - - 236.51 MB
'AutoMapper 10.0.0' 420.97 ms 23.266 ms 35.174 ms 87000.0000 - - 350.95 MB

从表中我们可以看出,即使在不使用高性能组件的情况下它的性能都可以获得 4 倍于 AutoMapper,却只需要 1/3 左右的内存占用,而在使用 Roslyn CompilerFEC (FastExpressionCompiler)Code generation 等组件后可以再进一步提升 2-3 倍的性能。 Code generation 方式几乎就是这个事儿极限了。你还有更快的手段吗?

在实际项目中的基本使用

首先从 Nuget 中引用最新版本的 Mapster 包:

dotnet add package Mapster

对象映射最多的场景就是两个实体定义的属性名是重叠对应的,那么此时的基本用法就非常简单:

var destObject = sourceObject.Adapt<Destination>();

注意我说的是实体定义,没有只限制类定义。Class、Record(有点小限制注意查阅官方文档)、Interface 等各种形式都可以哦,这是我非常喜欢的。当然了你的源是 IQueryable 的也可以!

不是类也不是接口,只是基本的简单类型是否可以呢?也可以!

var s = 123.Adapt<string>(); // 等同于: 123.ToString();

列表、数组、集合、包括各种接口的字典之间的映射,也可以: IList<T>, ICollection<T>, IEnumerable<T>, ISet<T>, IDictionary<TKey, TValue> 等等都可以!

只要C#支持类型转换的类型,那么在 Mapster 中也同样支持转换,而且像枚举与字符串之间的转换,.NET 自带的方式性能稍慢,Mapster 也针对性的做了优化,所以你实际生产中绝大部分就是类似上面这么一行代码就行了,够简单便捷吧 :D

在某些情况下,需要依赖注入,Mapster 提供了 IMapperMapper 来满足这个需求:

var result = mapper.Map<TDestination>(source);

映射配置

现实项目中难免会有一些自定义映射的需求,Mapster 提供了很强大的映射配置机制,可以通过映射配置解决你各种灵活需求。

我们可以使用 TypeAdapterConfig<TSource, TDestination>.NewConfig()TypeAdapterConfig<TSource, TDestination>.ForType() 配置类型映射;

注意当调用 NewConfig 方法时,将会覆盖已存在的类型映射配置。

TypeAdapterConfig<TSource, TDestination>
    .NewConfig()
    .Ignore(dest => dest.Age)
    .Map(dest => dest.FullName,
        src => string.Format("{0} {1}", src.FirstName, src.LastName));

当然了你想让自己配置全局有效,可以通过对 TypeAdapterConfig.GlobalSettings 进行设置处理。

你有一些场景需要有条件规则?没问题,可以通过 When 方法来实现:

TypeAdapterConfig.GlobalSettings
    .When((srcType, destType, mapType) => srcType == destType)
    .Ignore("Id");

上面这个配置的意思是,应用全局范围当任何一个映射的源类型和目标类型相同时,不映射 Id 属性。

新版本中对接口只读属性映射的增强

最近刚刚发布对 Mapster 7.3.0 带来了一些新的增强:

  • Switch expression by @SergerGood in #334
  • Upgrade packages by @SergerGood in #333
  • Include .NET 6.0 as Target Framework for Mapster.Tool by @kaizen365 in #390
  • Updated Sample Code in Readme by @CoSJay in #379
  • Simplify packaging and publishing NuGet packages, remove old framework monikers and upgrade to C# 10.0 by @andrerav in #405
  • Add ability to compile all mappings and then throw AggregateException by @MisterOzzy in #363
  • Init read-only properties when mapping with a non-readonly interface fixes #374 by @andrei-traktatovich in #375

其中最后一下对接口的只读属性映射增强,是我非常喜欢的,解决了在实际项目中的设计需求,省了不少事儿。

public interface ITarget
{
  int GetOnlyProperty {get;}
  int NormalProperty {get;set;}
}
public interface ITargetWithGetSetProperties
{
  int GetOnlyProperty {get;}
  int NormalProperty {get;set;}
}

上面这个代码中场景中,如果 ITarget 类型的对象的属性 GetOnlyProperty 带有一个非 0 值,并想 Map 为 ITargetWithGetSetProperties 类型的对象时,老版本会在映射后目标对象的 GetOnlyProperty 保留 int 类型的默认值 0,没做任何映射,新版本中解决了这个问题!

你可能会问“为什么会有这个需求?”,嗯,一个原因是因为接口可以多继承,而类只能单一继承,你品品,细品…… :D

其他

微信公众号文章不适合详细展开讨论和分享,本文主要是抛砖引玉。想详细了解这个框架的可以到官方代码库中去看一下 https://github.com/MapsterMapper/Mapster ,如果说英文阅读有点困难,可以到 https://github.com/rivenfx/Mapster-docs 看热心网友做到中文翻译版。