其实四年前就已经将手边专案由原本所使用的 AutoMapper 以 Mapster 取代了。
起初的原因是效能考量,因为 AutoMapper 的效能一直被人诟病,但也因为 AutoMapper 的优点在于功能丰富、配置设定灵活,能够处理複杂的 Mapping 需求,以致于我在带新人的时候还是以 AutoMapper 为主,但是前些日子得知 AutoMapper 在之后也将走上商业化(跟 Fluent Assertions 一样),所以就藉此来写篇文章简单介绍 Mapster。
AutoMapper 要转为商业授权
某天在一堆专案的商业逻辑连番轰炸后的下班归途中,开启手机滑着 Facebook 的讯息,看看最近同温层有没有什么新鲜事,然后就在「Cash Wu Geek」看到了这一篇
- 原始连结
AutoMapper 作者 Jimmy Board 的文章
- AutoMapper and MediatR Going Commercial
相关连结:
- AutoMapper, MediatR and MassTransit go Commercial! - Nick Chapsas (Youtube)
也因此现在应该有很多人开始準备要将手边专案里所使用的 AutoMapper 给替换掉,但 Mapping 工具有很多种,可以从以下的连结查到有很多种
- https://dotnet.libhunt.com/automapper-alternatives
例如:Mapster, Mapperly, AgileMapper, ExpressMapper, TinyMapper, ValueInjecter, EmitMapper 等
当然还有很多人是认为使用 Mapping 工具才是拖垮性能的根本原因之一(当然还有其他原因),所以就使用手动写 Mapping (Manaul Map) 的方式。
而我当时从 AutoMapper 要做转换也试过了很多种套件,在各种评估与考量后就选择使用 Mapster,以下就简单介绍怎么使用 Mapster。
使用 Mapster
- https://github.com/MapsterMapper/Mapster
- https://www.nuget.org/packages/Mapster/
README 下方有个效能的比较表(不过这张表也看了好多年,不知道目前是否还是这样呢?)
Mapster 的基本使用方式,就请各位自己去看 Wiki 文件:
- https://github.com/MapsterMapper/Mapster/wiki
- https://github.com/rivenfx/Mapster-docs (简体中文)
其实 Ian Chen 早在 2020/03 的时候就有写过一篇文章做介绍
- [NuGetPackage] 使用 Mapster 处理物件对应 | Ian Chen 心路历程
另外也列几篇简体中文的文章(因为正体中文有介绍 Mapster 实在不多)
- Mapster 高性能对象映射框架 - yswenli - 博客园
- C# Mapster 对象映射器(C#对象映射器) - 一颗花生豆 - 博客园
其他连结:
- Mapster Mapper: Fastest object to object Mapper | CodeNx
- Mapster, the best .NET mapper that you are (probably) not using | Nick Chapsas
- Using Mapster in ASP.NET Core Applications | Code Maze
我大部分的 Mapping 转换设定都不是很複杂,所以大部分都是简单的使用:
using Mapster;
using Sample.Domain.Entities;
using Sample.Service.Dto;
namespace Sample.Service.MapConfig;
public class ServiceMapRegister : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<ShipperModel, ShipperDto>().TwoWays();
}
}
using Mapster;
using Sample.Service.Dto;
using Sample.WebApplication.Models.InputParameters;
using Sample.WebApplication.Models.OutputModels;
namespace Sample.WebApplication.Infrastructure.MapConfig;
/// <summary>
/// class WebApplicationMapRegister
/// </summary>
public class WebApplicationMapRegister : IRegister
{
/// <summary>
/// Register
/// </summary>
/// <param name="config">config</param>
public void Register(TypeAdapterConfig config)
{
config.NewConfig<ShipperDto, ShipperOutputModel>();
config.NewConfig<ShipperParameter, ShipperDto>()
.Map(d => d.CompanyName, s => s.CompanyName)
.Map(d => d.Phone, s => s.Phone);
}
}
以一般情境的设定使用方式其实与 AutoMapper 差不了多少。
Mapster 的 Dependency Injection 设定
虽然 Mapper.DependencyInjection 有个 AddMapster() 静态扩充方法,但这个方法就只是个简单的方法
- https://github.com/MapsterMapper/Mapster/blob/master/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs
仅靠这个方法是无法有效处理 Mapster 的相依性注入设定,所以 Mapster 有在 Wiki 里写了怎么设定的文件:
- https://github.com/MapsterMapper/Mapster/wiki/Dependency-Injection
- https://github.com/rivenfx/Mapster-docs/blob/master/cn/Dependency-Injection.md
记得要安装 Mapster.DependencyInjection 这个 NuGet 套件
- https://www.nuget.org/packages/Mapster.DependencyInjection
而我们要安装 Mapster.DependencyInjection 套件,最主要是要注册 TypeAdapterConfig
与 ServiceMapper
,例如我在专案里的设定如下:
我分别在 Service 与 WebApplication 专案里会去继承实作 IRegister 的 ServiceMapRegister 与 WebApplicationMapRegister 类别,那么就需要将这两个类别的 Assembly 让 TypeAdapterConfig 去扫描以抓出有继承实作 IRegister 的类别,然后必须将 TypeAdapterConfig instance 注入设定的 Lifetime 必须为 Singleton
/// <summary>
/// Class MapsterServiceCollectionExtensions
/// </summary>
public static class MapsterServiceCollectionExtensions
{
/// <summary>
/// Add Mapster
/// </summary>
/// <param name="services">services</param>
/// <returns></returns>
public static IServiceCollection AddMapster(this IServiceCollection services)
{
var config = new TypeAdapterConfig();
var serviceAssembly = typeof(ServiceMapRegister).Assembly;
var webAssembly = typeof(WebApplicationMapRegister).Assembly;
config.Scan(serviceAssembly);
config.Scan(webAssembly);
services.AddSingleton(config);
services.AddScoped<IMapper, ServiceMapper>();
return services;
}
}
为什么需要 Mapster.DependencyInjection?
在一个使用相依性注入(DI)的 ASP.NET Core 专案中,原生 Mapster 只有提供了简单的 AddMapster() 方法(基本上只是注册 IMapper 的实作),而 Mapster.DependencyInjection 则提供了更进一步的支援,使我们能够利用 DI 容器注入映射器,同时方便地管理映射配置。
ServiceMapper 是什么?有何功能?
ServiceMapper 是 Mapster.DependencyInjection 提供的 IMapper 实作,它的主要功能如下:
- 封装映射配置
ServiceMapper 持有一个 TypeAdapterConfig 实例,该配置集中管理所有映射规则,并依据这些规则执行对象转换。 - 整合依赖注入
透过 DI 容器注册 ServiceMapper(通常採用 Scoped 生命週期),使得在专案中可以直接注入并使用 IMapper,并且 ServiceMapper 可借助 DI 解决映射过程中其他服务的依赖问题 - 提高维护性与一致性
将映射配置与转换逻辑集中管理,降低了映射规则散布在各个地方的风险,这对于大型专案及持续演进的系统来说非常重要。
改写 MapsterServiceCollectionExtensions 静态扩充类别的 AddMapSter 方法
上面的 MapsterServiceCollectionExtensions 的 AddMapSter 方法实际上已经是可以使用了,但是会想要改写的其中一个原因是方法名称与 Mapster 所提供的 AddMapster 方法名称相近而容易造成混淆,另外就是希望将方法里直接指定 Assembly 的部分改为从 Assemblies 里扫描载入,并且也想要保留可以指定过滤 Assembly 名称条件的方式。
实作 AssemblyHelper 类别以及 GetReferencedAssemblies 方法
using System.Reflection;
using Microsoft.Extensions.DependencyModel;
namespace Sample.WebApplication.Infrastructure.Helpers;
/// <summary>
/// class AssemblyHelper
/// </summary>
public static class AssemblyHelper
{
/// <summary>
/// 取得符合指定前缀或后缀关键字的 Assembly 清单
/// </summary>
/// <param name="prefixNames">Assembly 名称前缀字元阵列(如果为 null 或空阵列,则不做前缀筛选)</param>
/// <param name="suffixNames">Assembly 名称后缀字元阵列(如果为 null 或空阵列,则不做后缀筛选)</param>
/// <returns>符合条件的 Assembly 清单</returns>
public static IEnumerable<Assembly> GetReferencedAssemblies(
IEnumerable<string> prefixNames = null,
IEnumerable<string> suffixNames = null)
{
var assemblies = DependencyContext.Default.RuntimeLibraries
.Where(library => IsMatch(library.Name, prefixNames, suffixNames))
.Select(library =>
{
try
{
return Assembly.Load(new AssemblyName(library.Name));
}
catch
{
// 若无法载入,则忽略此 Assembly
return null;
}
})
.Where(assembly => assembly is not null)
.ToList();
return assemblies;
}
/// <summary>
/// 检查是否符合
/// </summary>
/// <param name="libraryName">libraryName</param>
/// <param name="prefixNames">prefixNames</param>
/// <param name="suffixNames">suffixNames</param>
/// <returns></returns>
private static bool IsMatch(string libraryName, IEnumerable<string> prefixNames, IEnumerable<string> suffixNames)
{
// 检查前缀条件:若未指定则视同通过
var matchPrefix = prefixNames is null
|| !prefixNames.Any()
|| prefixNames.Any(prefix => libraryName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
// 检查后缀条件:若未指定则视同通过
var matchSuffix = suffixNames is null
|| !suffixNames.Any()
|| suffixNames.Any(suffix => libraryName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase));
return matchPrefix && matchSuffix;
}
}
这里的 GetReferencedAssemblies 方法利用 DependencyContext.Default.RuntimeLibraries 来取得目前参考的 Assembly,再根据设定的前缀与后缀筛选出特定的组件(如果有设定)。
为何不是使用 AppDomain.CurrentDomain.GetAssemblies() 而是使用 DependencyContext.Default.RuntimeLibraries?
AppDomain.CurrentDomain.GetAssemblies() 的限制:
当使用 AppDomain.CurrentDomain.GetAssemblies() 时,只有目前已经载入到 AppDomain 中的 Assembly 会被回传。如果某个专案(例如 Sample.Service)虽被 Sample.WebApplication 参照,但在执行期间没有任何程式码使用到它,则该 Assembly 可能不会被自动载入,就不会出现在 GetAssemblies() 的结果中。
DependencyContext.Default.RuntimeLibraries 的特性:
DependencyContext.Default.RuntimeLibraries 是从编译产生的 .deps.json 档案里读取参考资讯,会列出所有专案所依赖的类别库,无论它们是否实际被载入。因此,使用 DependencyContext 可以得到全部参考(包括 Sample.Service 以及所有 NuGet 套件)的列表(但这也可能造成你撷取到许多不必要的外部组件)。
相关连结:
- https://learn.microsoft.com/zh-tw/dotnet/api/system.appdomain.getassemblies
- https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.dependencymodel.dependencycontext
- https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.dependencymodel.dependencycontext.default
- https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.dependencymodel.dependencycontext.runtimelibraries
- 新手进阶路上的坑AppDomain.CurrentDomain.GetAssemblies() - Cameron - 博客园
- Replacing AppDomain in .Net Core - Michael Whelan - behaviour driven blog
修改后的 MapsterServiceCollectionExtensions 静态扩充类别与 AddMapsterDependencyInjection 方法
using System.Reflection;
using Mapster;
using MapsterMapper;
using Sample.WebApplication.Infrastructure.Helpers;
namespace Sample.WebApplication.Infrastructure.ServiceCollections;
/// <summary>
/// Class MapsterServiceCollectionExtensions
/// </summary>
public static class MapsterServiceCollectionExtensions
{
/// <summary>
/// Add the Mapster Dependency Injection
/// </summary>
/// <param name="services">services</param>
/// <returns></returns>
public static IServiceCollection AddMapsterDependencyInjection(this IServiceCollection services)
{
var config = new TypeAdapterConfig();
// 利用 AssemblyHelper 取得符合指定前缀与后缀的 Assembly
var assemblies = AssemblyHelper.GetReferencedAssemblies(
prefixNames: ["Sample"],
suffixNames: ["Service", "WebApplication"]);
// 也可以不指定前缀字、后缀字,直接取得
// var assemblies = AssemblyHelper.GetReferencedAssemblies();
// 进一步过滤出至少包含一个实作 IRegister 的 Assembly
var containsIRegisterImplementationAssemblies = assemblies.Where(ContainsIRegisterImplementation).ToArray();
// 扫描这些 Assembly 以注册 Mapster 的映射设定
config.Scan(containsIRegisterImplementationAssemblies);
services.AddSingleton(config);
services.AddScoped<IMapper, ServiceMapper>();
return services;
}
/// <summary>
/// 过滤至少包含一个实作 IRegister 的类别的 Assembly
/// </summary>
/// <param name="assembly">The assembly</param>
/// <returns></returns>
private static bool ContainsIRegisterImplementation(Assembly assembly)
{
try
{
return assembly.GetTypes()
.Any(t => typeof(IRegister).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types
.Where(t => t is not null)
.Any(t => typeof(IRegister).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
}
}
}
在 AddMapsterDependencyInjection 方法里可以使用 AssemblyHelper.GetReferencedAssemblies 方法取得 Assemblies (不论是有使用过滤条件或不输入条件),会再透过 ContainsIRegisterImplementation 方法进一步确定这些组件里至少有一个类别有实作 IRegister 介面,最后只对有包含实作 IRegister 的 Assembly 去做扫描和注册 Mapping 设定。
最后在 Program.cs 里使用 AddMapsterDependencyInjection 方法就完成 DI 设定
测试专案的 Mapster 处理
在过去的文章「使用 AutoFixture.AutoData 来改写以前的的测试程式码」里就曾经有介绍过怎么在使用 AutoFixture.AutoData 去解决 Mapster 的相依注入,可以在文章里寻找关键字「MapsterMapperCustomization」就可以看到,这边也直接贴出程式码
using AutoFixture;
using Mapster;
using MapsterMapper;
using Sample.Service.MapConfig;
namespace Sample.ServiceTests.AutoFixtureConfigurations;
/// <summary>
/// class MapsterMapperCustomization
/// </summary>
public class MapsterMapperCustomization : ICustomization
{
/// <summary>
/// Customizes the fixture
/// </summary>
/// <param name="fixture">The fixture</param>
public void Customize(IFixture fixture)
{
fixture.Register(() => this.Mapper);
}
private IMapper _mapper;
private IMapper Mapper
{
get
{
if (this._mapper is not null)
{
return this._mapper;
}
var typeAdapterConfig = new TypeAdapterConfig();
typeAdapterConfig.Scan(typeof(ServiceMapRegister).Assembly);
this._mapper = new Mapper(typeAdapterConfig);
return this._mapper;
}
}
}
对于 Mapping Tools 的思考
儘管 Mapster、AutoMapper 等工具能够大幅减少手写 Mapping 程式码,但也有不少声音主张不要过度依赖映射工具。以下是一些常见观点:
- 性能隐忧
Mapping Tools 透过反射或动态产生程式码来实现,当处理大量资料或在高併发环境下,可能会成为系统瓶颈。 - 隐性错误
Mapping Tools 将逻辑隐藏在配置与预设规则后,当 Model 类别变更时,常常在编译期间无法捕捉错误,而只能在执行期间才出现映射失败或资料错乱的状况。 - 可读性与维护性
自动映射虽然能够节省程式码,但映射规则常常分散且有时难以阅读,使得新团队成员理解业务逻辑时会需要花费较多心力。
所以对于要不要使用 AutoMapper 或 Mapster 这类的 Mapping 工具,团队与开发者要自己考量利弊。
后记
有关各个 Mapping 工具的效能比较,除了 Mapster 在 Github Repo 里所提供那张表之外,也另外找到了一篇文章,而且还有提供原始码,所以有兴趣的可以去做个了解。
- Mapping Experiment in .net core- AutoMapper, ExpressMapper, Mapster & Manual mapping | Matija Katadzic | Linkedin
- https://github.com/matijakatadzic/MappingExperiments
source code 专案为 .NET 5,专案里面所使用的 AutoMapper 版本为 11.0.1,Mapster 的版本为 7.3.0,Expressmapper 为 1.9.1
由上面的表来看,Mapster 搭配 Code Generation 的版本所跑出来的效能还比手动来得快
问了 ChatGPT 后,给我以下的回覆
然后这是另外一篇 Performance 的比较,把几个主要的 Mapping 工具都做了比较
- https://github.com/mjebrahimi/DotNet-Mappers-Benchmark
看了上面的比较后,或许也可以参考看看 Mapperly
- https://github.com/riok/mapperly
- https://mapperly.riok.app/docs/intro/
一开始就直接说「Mapperly is a .NET source generator for generating object mappings.」
但不知道用 Mapster.Tool 採用 Code Ganarator 与 Mapperly 的比较会是谁的效能会比较好呢?
以后再来好好研究研究…
纯粹是在写兴趣的,用写程式、写文章来抒解工作压力