其實四年前就已經將手邊專案由原本所使用的 AutoMapper 以 Mapster 取代了。
起初的原因是效能考量,因為 AutoMapper 的效能一直被人詬病,但也因為 AutoMapper 的優點在於功能豐富、配置設定靈活,能夠處理複雜的 Mapping 需求,以致於我在帶新人的時候還是以 AutoMapper 為主,但是前些日子得知 AutoMapper 在之後也將走上商業化(跟 Fluent Assertions 一樣),所以就藉此來寫篇文章簡單介紹 Mapster。
AutoMapper 要轉為商業授權
某天在一堆專案的商業邏輯連番轟炸後的下班歸途中,開啟手機滑著 Facebook 的訊息,看看最近同溫層有沒有什麼新鮮事,然後就在「Cash Wu Geek」看到了這一篇

AutoMapper 作者 Jimmy Board 的文章
相關連結:
也因此現在應該有很多人開始準備要將手邊專案裡所使用的 AutoMapper 給替換掉,但 Mapping 工具有很多種,可以從以下的連結查到有很多種
例如:Mapster, Mapperly, AgileMapper, ExpressMapper, TinyMapper, ValueInjecter, EmitMapper 等
當然還有很多人是認為使用 Mapping 工具才是拖垮性能的根本原因之一(當然還有其他原因),所以就使用手動寫 Mapping (Manaul Map) 的方式。
而我當時從 AutoMapper 要做轉換也試過了很多種套件,在各種評估與考量後就選擇使用 Mapster,以下就簡單介紹怎麼使用 Mapster。
使用 Mapster

README 下方有個效能的比較表(不過這張表也看了好多年,不知道目前是否還是這樣呢?)

Mapster 的基本使用方式,就請各位自己去看 Wiki 文件:
其實 Ian Chen 早在 2020/03 的時候就有寫過一篇文章做介紹
另外也列幾篇簡體中文的文章(因為正體中文有介紹 Mapster 實在不多)
其他連結:
- 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() 靜態擴充方法,但這個方法就只是個簡單的方法

僅靠這個方法是無法有效處理 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 套件
而我們要安裝 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 工具都做了比較

看了上面的比較後,或許也可以參考看看 Mapperly
一開始就直接說「Mapperly is a .NET source generator for generating object mappings.」
但不知道用 Mapster.Tool 採用 Code Ganarator 與 Mapperly 的比較會是誰的效能會比較好呢?
以後再來好好研究研究…
最後補個 AssemblyHelper 類別裡 GetReferencedAssemblies 方法的單元測試
using FluentAssertions;
using Sample.WebApplication.Infrastructure.Helpers;
namespace Sample.WebApplicationTests.Infrastructure.Helpers;
public class AssemblyHelperTests
{
[Fact]
public void GetReferencedAssemblies_方法未未帶入前綴與後綴關鍵字_應回傳所有參考的組件()
{
// act
var actual = AssemblyHelper.GetReferencedAssemblies();
// assert
actual.Should().NotBeNull();
actual.Should().NotBeEmpty();
}
[Fact]
public void GetReferencedAssemblies_帶入不存在的前綴名稱_應回傳空的集合()
{
// arrange
const string prefixName = "Impossible";
// act
var actual = AssemblyHelper.GetReferencedAssemblies(prefixNames: [prefixName]);
// assert
actual.Should().NotBeNull();
actual.Should().BeEmpty();
}
[Fact]
public void GetReferencedAssemblies_帶入不存在的後綴名稱_應回傳空的集合()
{
// arrange
const string suffixName = "Impossible";
// act
var actual = AssemblyHelper.GetReferencedAssemblies(prefixNames: [suffixName]);
// assert
actual.Should().NotBeNull();
actual.Should().BeEmpty();
}
[Fact]
public void GetReferencedAssemblies_前綴字帶入System_應回傳名稱前綴符合System的組件()
{
// arrange
const string prefixName = "System";
// act
var actual = AssemblyHelper.GetReferencedAssemblies(prefixNames: [prefixName]);
// assert
actual.Should().NotBeNull();
actual.Should().NotBeEmpty();
actual.All(x => x.GetName().Name.StartsWith(prefixName, StringComparison.OrdinalIgnoreCase)).Should().BeTrue();
}
[Fact]
public void GetReferencedAssemblies_後綴字帶入Core_應回傳名稱結尾符合Core的組件()
{
// arrange
const string suffixName = "Core";
// act
var actual = AssemblyHelper.GetReferencedAssemblies(suffixNames: [suffixName]);
// assert
actual.Should().NotBeNull();
actual.Should().NotBeEmpty();
actual.All(x => x.GetName().Name.EndsWith(suffixName, StringComparison.OrdinalIgnoreCase)).Should().BeTrue();
}
[Fact]
public void GetReferencedAssemblies_前綴字帶入System_後綴字帶入Core_應回傳空的集合()
{
// arrange
const string prefixName = "System";
const string suffixName = "Core";
// act
var actual = AssemblyHelper.GetReferencedAssemblies(prefixNames: [prefixName], suffixNames: [suffixName]);
// assert
actual.Should().NotBeNull();
actual.Should().BeEmpty();
}
[Fact]
public void GetReferencedAssemblies_前綴字帶入Sample_後綴字帶入Service_集合應只有一個組件()
{
// arrange
const string prefixName = "Sample";
const string suffixName = "Service";
// act
var actual = AssemblyHelper.GetReferencedAssemblies(prefixNames: [prefixName], suffixNames: [suffixName]);
// assert
actual.Should().NotBeNull();
actual.Should().NotBeEmpty();
actual.Should().HaveCount(1);
actual.All(x =>
{
var name = x.GetName().Name;
return name.StartsWith(prefixName, StringComparison.OrdinalIgnoreCase)
&& name.EndsWith(suffixName, StringComparison.OrdinalIgnoreCase);
})
.Should().BeTrue();
}
[Fact]
public void GetReferencedAssemblies_前綴字帶入Sample_後綴字帶入WebApplication_集合應只有一個組件()
{
// arrange
const string prefixName = "Sample";
const string suffixName = "WebApplication";
// act
var actual = AssemblyHelper.GetReferencedAssemblies(prefixNames: [prefixName], suffixNames: [suffixName]);
// assert
actual.Should().NotBeNull();
actual.Should().NotBeEmpty();
actual.Should().HaveCount(1);
actual.All(x =>
{
var name = x.GetName().Name;
return name.StartsWith(prefixName, StringComparison.OrdinalIgnoreCase)
&& name.EndsWith(suffixName, StringComparison.OrdinalIgnoreCase);
})
.Should().BeTrue();
}
}
以上
純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力