另一種映射工具 - Mapperly

  • 201
  • 0
  • C#
  • 2025-04-13

Mapperly 在以前找尋替代 AutoMapper 的時候就有看過,但當時著重在與 AutoMapper 設定與操作習慣相近的替代套件,所以對於 Mapperly 就沒有太多的關注。

直到寫了「替換映射工具 - 使用 Mapster」這篇文章後才稍微去看看 Mapperly,發現到它和 AutoMapper, Mapster 雖然都是屬於 Mapping 工具,都是做物件對映轉換的處理,但設定與使用上就有蠻大的差別,所以寫篇文章做個簡單的紀錄。

.NET Object Mappers Benchmark

在寫「替換映射工具 - 使用 Mapster」這篇文章的最後有引用參考了「.NET Object Mappers Benchmark」的資料,對於幾種主要常見的 Mapping 工具跑了 benchmark 做效能的比對,物件轉換的工具有:

執行結果(以 Categories 為 Class 來看):

  • 最快的是 Mapperly
  • 第二快的是 Mapster

但看了一下原始碼,發現到裡面使用 Mapster 的轉換並不是使用 Source Genarator 的 Mapster.Tool,而是使用 Mapster 的 Adpt<T>()  方法。而 Mapperly 是採用 Source Ganerator 產生 Mapping 的程式碼,所以 Mapster 與 Mapperly 相比就會落下風,原本想要好好研究一下 Mapster.Tool 這個也一樣使用 Source Generator 功能在編譯時期生成 Mapping  程式碼的工具,但是稍做一下瞭解之後發現到 … 超麻煩的,所以最後果斷放棄然後直接去把時間拿來認識 Mapperly。

Mapster.Tool 套件是作為 Dotnet Tool 發行的,而不是普通的 NuGet 套件,因此不能直接以 PackageReference 的方式引用到像你這樣的應用程式專案中。
它是一個命令列工具,而不是普通的函式庫。Dotnet 工具必須以 dotnet tool 的方式安裝,而不能直接放在專案的 PackageReference 中。
在 CI/CD pipeline 流程中必須要做一些額外的設定,以確保工具能夠被正確下載和使用。

Mapperly

Mapperly 是一個用於產生物件映射的 .NET 來源產生器。
由於 Mapperly 在建置時會建立映射程式碼,因此運行時的開銷很小。尤其是,生成的程式碼具有完美的可讀性,使您可以輕鬆驗證生成的映射程式碼。

相關的影片介紹:

相關文章介紹:

Mapperly 是一個針對 .NET 框架的 Source Ganarator,用來自動生成物件映射(object mapping)程式碼。與傳統的 AutoMapper 不同,Mapperly 的主要特性在於「編譯時期產生程式碼」,這表示在建置(build)時就將所需的映射程式碼生成完畢,因而在執行期間不會有額外的效能負擔。

編譯時生成映射程式碼

  • 效能優化:由於映射邏輯是編譯時期間產生的,所以執行期間不需要反射或動態解析。這使得 Mapperly 在執行時擁有極低的效能損耗,尤其適合對於執行效能要求很高的生產環境。
  • 可讀性:生成的程式碼是標準的 C# 程式碼,開發者可以直接閱讀、Debug。這對於需要驗證或調整映射邏輯的需求是相當有幫助。

與 AutoMapper 的比較

  • 執行時期 vs 編譯時期:AutoMapper 通常在執行時期進行映射設定和解析,這可能導致在執行時期出現額外的效能負擔。而 Mapperly 則在編譯時期就解決了這個問題。
  • Debug 和錯誤追蹤:因為生成的程式碼具有可讀性,開發者能夠更容易追蹤映射過程中可能出現的錯誤和問題。
  • 使用情境:如果應用場景對於執行效能和編譯時期錯誤檢查有較高要求,Mapperly 是一個很好的選擇;相反地,如果更看重程式碼直接開發和可以自行調整映射邏輯配置,AutoMapper 可能會更方便些。

優點與潛在挑戰

  • 優點:
    • 高效能:編譯時期生成程式碼意味著更快的映射操作。
    • 可讀性與維護性: 開發者可以檢查生成的 C# 程式碼,進行必要的調整或優化。
    • 早期錯誤捕捉: 由於大部分配置工作都在編譯時期間完成,因此可以及早發現問題,減少運行時錯誤。
  • 挑戰:
    • 學習曲線: 對於習慣於 AutoMapper 運行時配置的開發者,可能需要一些時間來適應 Mapperly 的編譯時期生成模式。
    • 靈活性: 在某些需要極高動態映射配置的情境下,雖然 Mapperly 支持一定程度的自定義,但可能不如 AutoMapper 那麼靈活。

 

專案使用 Mapperly

將一個原本使用 Mapster 的專案拿來練習,因為並沒有太複雜的 Mapping 設定,裡面只有四個類別要做轉換

public class ServiceMapRegister : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<ShipperModel, ShipperDto>().TwoWays();
    }
}
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);
    }
}

首先分別在 Service 與 WebApplication 專案安裝 NuGet 套件 - Riok.Mapperly

dotnet add package Riok.Mapperly

在 Service 專案裡建立 Mappers 資料夾,然後建立 ShipperMapper 類別

using Riok.Mapperly.Abstractions;
using Sample.Domain.Entities;
using Sample.Service.Dto;

namespace Sample.Service.Mappers;

/// <summary>
/// class Mapper
/// </summary>
/// <remarks>
/// 類別使用 Mapper 屬性,這個屬性表明該類別由 Mapperly 處理,並會根據定義的部分方法生成實作程式碼
/// 類別必須為 static 而且是 partial (靜態部分類別)
/// </remarks>
[Mapper]
public static partial class ShipperMapper
{
    // 以下定義映射轉換的方法,方法也必須是 static 和 partial (靜態部分方法 partial methods)
    // MapToDto 與 MapToModel 靜態部分方法,Mapperly 在編譯時會自動產生具體實作的程式碼。生成的程式碼會將相同屬性名稱的對應欄位自動映射。

    /// <summary>
    /// 將 ShipperDto 映射到 ShipperModel
    /// </summary>
    /// <param name="dto">dto</param>
    /// <returns></returns>
    public static partial ShipperModel MapToModel(ShipperDto dto);

    /// <summary>
    /// 將 ShipperModel 映射到 ShipperDto
    /// </summary>
    /// <param name="model">model</param>
    /// <returns></returns>
    public static partial ShipperDto MapToDto(ShipperModel model);
}

接著改寫 ShipperService 類別,這個類別原本是使用 Mapster 的 IMapper 進行物件的映射轉換,以下是改寫前的程式碼

using MapsterMapper;
using Sample.Domain.Entities;
using Sample.Domain.Misc;
using Sample.Domain.Repositories;
using Sample.Domain.Validation;
using Sample.Service.Dto;
using Sample.Service.Interface;
using Throw;

namespace Sample.Service.Implements;

/// <summary>
/// class ShipperService.
/// </summary>
public class ShipperService : IShipperService
{
    private readonly IMapper _mapper;

    private readonly IShipperRepository _shipperRepository;

    /// <summary>
    /// Initializes a new instance of the <see cref="ShipperService"/> class.
    /// </summary>
    /// <param name="mapper">The mapper</param>
    /// <param name="shipperRepository">The shipperRepository</param>
    public ShipperService(IMapper mapper, IShipperRepository shipperRepository)
    {
        this._mapper = mapper;
        this._shipperRepository = shipperRepository;
    }

    //-----------------------------------------------------------------------------------------

    /// <summary>
    /// 以 ShipperId 查詢資料是否存在
    /// </summary>
    /// <param name="shipperId">shipperId</param>
    /// <returns></returns>
    public async Task<bool> IsExistsAsync(int shipperId)
    {
        shipperId.Throw().IfLessThanOrEqualTo(0);
        var exists = await this._shipperRepository.IsExistsAsync(shipperId);
        return exists;
    }

    /// <summary>
    /// 以 ShipperId 查詢資料是否存在
    /// </summary>
    /// <param name="shipperId">shipperId</param>
    /// <returns></returns>
    public async Task<ShipperDto> GetAsync(int shipperId)
    {
        shipperId.Throw().IfLessThanOrEqualTo(0);

        var exists = await this._shipperRepository.IsExistsAsync(shipperId);
        if (!exists)
        {
            return null;
        }

        var model = await this._shipperRepository.GetAsync(shipperId);
        var shipper = this._mapper.Map<ShipperModel, ShipperDto>(model);
        return shipper;
    }

    /// <summary>
    /// 取得 Shipper 的資料總數
    /// </summary>
    /// <returns></returns>
    public async Task<int> GetTotalCountAsync()
    {
        var totalCount = await this._shipperRepository.GetTotalCountAsync();
        return totalCount;
    }

    /// <summary>
    /// 取得所有 Shipper 資料
    /// </summary>
    /// <returns></returns>
    public async Task<IEnumerable<ShipperDto>> GetAllAsync()
    {
        var models = await this._shipperRepository.GetAllAsync();
        var shippers = this._mapper.Map<IEnumerable<ShipperDto>>(models);
        return shippers;
    }

    /// <summary>
    /// 取得所有 Shipper 資料 (分頁)
    /// </summary>
    /// <param name="from">From.</param>
    /// <param name="size">The size.</param>
    /// <returns></returns>
    public async Task<IEnumerable<ShipperDto>> GetCollectionAsync(int @from, int size)
    {
        from.Throw().IfLessThanOrEqualTo(0);
        size.Throw().IfLessThanOrEqualTo(0);

        var totalCount = await this.GetTotalCountAsync();
        if (totalCount.Equals(0))
        {
            return Enumerable.Empty<ShipperDto>();
        }

        if (from > totalCount)
        {
            return Enumerable.Empty<ShipperDto>();
        }

        var models = await this._shipperRepository.GetCollectionAsync(from, size);
        var shippers = this._mapper.Map<IEnumerable<ShipperModel>, IEnumerable<ShipperDto>>(models);
        return shippers;
    }

    /// <summary>
    /// 以 CompanyName or Phone 查詢符合條件的資料
    /// </summary>
    /// <param name="companyName">Name of the company.</param>
    /// <param name="phone">The phone.</param>
    /// <returns></returns>
    public async Task<IEnumerable<ShipperDto>> SearchAsync(string companyName, string phone)
    {
        if (string.IsNullOrWhiteSpace(companyName) && string.IsNullOrWhiteSpace(phone))
        {
            throw new ArgumentException("companyName 與 phone 不可都為空白");
        }
        
        var totalCount = await this.GetTotalCountAsync();
        if (totalCount.Equals(0))
        {
            return Enumerable.Empty<ShipperDto>();
        }
        
        var models = await this._shipperRepository.SearchAsync(companyName, phone);
        var shippers = this._mapper.Map<IEnumerable<ShipperDto>>(models);
        return shippers;
    }

    /// <summary>
    /// 新增
    /// </summary>
    /// <param name="shipper">The shipper.</param>
    /// <returns></returns>
    public async Task<IResult> CreateAsync(ShipperDto shipper)
    {
        ModelValidator.Validate(shipper, nameof(shipper));

        var model = this._mapper.Map<ShipperDto, ShipperModel>(shipper);
        var result = await this._shipperRepository.CreateAsync(model);
        return result;
    }

    /// <summary>
    /// 修改
    /// </summary>
    /// <param name="shipper">The shipper.</param>
    /// <returns></returns>
    public async Task<IResult> UpdateAsync(ShipperDto shipper)
    {
        ModelValidator.Validate(shipper, nameof(shipper));

        IResult result = new Result(false);

        var exists = await this._shipperRepository.IsExistsAsync(shipper.ShipperId);
        if (exists is false)
        {
            result.Message = "shipper not exists";
            return result;
        }

        var model = this._mapper.Map<ShipperDto, ShipperModel>(shipper);
        result = await this._shipperRepository.UpdateAsync(model);
        return result;
    }

    /// <summary>
    /// 刪除
    /// </summary>
    /// <param name="shipperId">shipperId</param>
    /// <returns></returns>
    public async Task<IResult> DeleteAsync(int shipperId)
    {
        shipperId.Throw().IfLessThanOrEqualTo(0);

        IResult result = new Result(false);

        var exists = await this._shipperRepository.IsExistsAsync(shipperId);
        if (exists is false)
        {
            result.Message = "shipper not exists";
            return result;
        }

        result = await this._shipperRepository.DeleteAsync(shipperId);
        return result;
    }
}

現在改用 Mapperly 所以就不再需要注入 IMapper,然後各個方法裡的物件映射轉換都改為使用 ShipperMapper,以下是改寫後的程式碼

using MapsterMapper;
using Sample.Domain.Entities;
using Sample.Domain.Misc;
using Sample.Domain.Repositories;
using Sample.Domain.Validation;
using Sample.Service.Dto;
using Sample.Service.Interface;
using Sample.Service.Mappers;
using Throw;

namespace Sample.Service.Implements;

/// <summary>
/// class ShipperService.
/// </summary>
public class ShipperService : IShipperService
{
    private readonly IShipperRepository _shipperRepository;

    /// <summary>
    /// Initializes a new instance of the <see cref="ShipperService"/> class.
    /// </summary>
    /// <param name="shipperRepository">The shipperRepository</param>
    public ShipperService(IShipperRepository shipperRepository)
    {
        this._shipperRepository = shipperRepository;
    }

    //-----------------------------------------------------------------------------------------

    /// <summary>
    /// 以 ShipperId 查詢資料是否存在
    /// </summary>
    /// <param name="shipperId">shipperId</param>
    /// <returns></returns>
    public async Task<bool> IsExistsAsync(int shipperId)
    {
        shipperId.Throw().IfLessThanOrEqualTo(0);
        var exists = await this._shipperRepository.IsExistsAsync(shipperId);
        return exists;
    }

    /// <summary>
    /// 以 ShipperId 查詢資料是否存在
    /// </summary>
    /// <param name="shipperId">shipperId</param>
    /// <returns></returns>
    public async Task<ShipperDto> GetAsync(int shipperId)
    {
        shipperId.Throw().IfLessThanOrEqualTo(0);

        var exists = await this._shipperRepository.IsExistsAsync(shipperId);
        if (!exists)
        {
            return null;
        }

        var model = await this._shipperRepository.GetAsync(shipperId);
        var shipper = ShipperMapper.MapToDto(model);
        return shipper;
    }

    /// <summary>
    /// 取得 Shipper 的資料總數
    /// </summary>
    /// <returns></returns>
    public async Task<int> GetTotalCountAsync()
    {
        var totalCount = await this._shipperRepository.GetTotalCountAsync();
        return totalCount;
    }

    /// <summary>
    /// 取得所有 Shipper 資料
    /// </summary>
    /// <returns></returns>
    public async Task<IEnumerable<ShipperDto>> GetAllAsync()
    {
        var models = await this._shipperRepository.GetAllAsync();
        var shippers = models.Select(ShipperMapper.MapToDto);
        return shippers;
    }

    /// <summary>
    /// 取得所有 Shipper 資料 (分頁)
    /// </summary>
    /// <param name="from">From.</param>
    /// <param name="size">The size.</param>
    /// <returns></returns>
    public async Task<IEnumerable<ShipperDto>> GetCollectionAsync(int @from, int size)
    {
        from.Throw().IfLessThanOrEqualTo(0);
        size.Throw().IfLessThanOrEqualTo(0);

        var totalCount = await this.GetTotalCountAsync();
        if (totalCount.Equals(0))
        {
            return [];
        }

        if (from > totalCount)
        {
            return [];
        }

        var models = await this._shipperRepository.GetCollectionAsync(from, size);
        var shippers = models.Select(ShipperMapper.MapToDto);
        return shippers;
    }

    /// <summary>
    /// 以 CompanyName or Phone 查詢符合條件的資料
    /// </summary>
    /// <param name="companyName">Name of the company.</param>
    /// <param name="phone">The phone.</param>
    /// <returns></returns>
    public async Task<IEnumerable<ShipperDto>> SearchAsync(string companyName, string phone)
    {
        if (string.IsNullOrWhiteSpace(companyName) && string.IsNullOrWhiteSpace(phone))
        {
            throw new ArgumentException("companyName 與 phone 不可都為空白");
        }

        var totalCount = await this.GetTotalCountAsync();
        if (totalCount.Equals(0))
        {
            return [];
        }

        var models = await this._shipperRepository.SearchAsync(companyName, phone);
        var shippers = models.Select(ShipperMapper.MapToDto);
        return shippers;
    }

    /// <summary>
    /// 新增
    /// </summary>
    /// <param name="shipper">The shipper.</param>
    /// <returns></returns>
    public async Task<IResult> CreateAsync(ShipperDto shipper)
    {
        ModelValidator.Validate(shipper, nameof(shipper));

        var model = ShipperMapper.MapToModel(shipper);
        var result = await this._shipperRepository.CreateAsync(model);
        return result;
    }

    /// <summary>
    /// 修改
    /// </summary>
    /// <param name="shipper">The shipper.</param>
    /// <returns></returns>
    public async Task<IResult> UpdateAsync(ShipperDto shipper)
    {
        ModelValidator.Validate(shipper, nameof(shipper));

        IResult result = new Result(false);

        var exists = await this._shipperRepository.IsExistsAsync(shipper.ShipperId);
        if (exists is false)
        {
            result.Message = "shipper not exists";
            return result;
        }

        var model = ShipperMapper.MapToModel(shipper);
        result = await this._shipperRepository.UpdateAsync(model);
        return result;
    }

    /// <summary>
    /// 刪除
    /// </summary>
    /// <param name="shipperId">shipperId</param>
    /// <returns></returns>
    public async Task<IResult> DeleteAsync(int shipperId)
    {
        shipperId.Throw().IfLessThanOrEqualTo(0);

        IResult result = new Result(false);

        var exists = await this._shipperRepository.IsExistsAsync(shipperId);
        if (exists is false)
        {
            result.Message = "shipper not exists";
            return result;
        }

        result = await this._shipperRepository.DeleteAsync(shipperId);
        return result;
    }
}

因為有寫單元測試與整合測試,就直接跑測試來驗證這樣的調整與使用 Mapperly 所產生的映射轉換程式碼是否讓應用程式都還能正確地執行

接著繼續調整 WbApplication 的程式碼

這邊會用到 Mapperly 的 Flattening and unflattening

在 WebApplication 的 Infrastructure 資料夾下建立 Mappers 資料夾,然後建立 ShipperMapper 類別

using Riok.Mapperly.Abstractions;
using Sample.Service.Dto;
using Sample.WebApplication.Models.InputParameters;
using Sample.WebApplication.Models.OutputModels;

namespace Sample.WebApplication.Infrastructure.Mappers;

/// <summary>
/// class Mapper
/// </summary>
/// <remarks>
/// 類別使用 Mapper 屬性,這個屬性表明該類別由 Mapperly 處理,並會根據定義的部分方法生成實作程式碼
/// 類別必須為 static 而且是 partial (靜態部分類別)
/// </remarks>
[Mapper]
public static partial class ShipperMapper
{
    /// <summary>
    /// 將 ShipperDto 映射到 ShipperOutputModel
    /// </summary>
    /// <param name="dto"></param>
    /// <returns></returns>
    public static partial ShipperOutputModel MapToOutputModel(ShipperDto dto);

    /// <summary>
    /// 將 ShipperParameter 映射到 ShipperDto
    /// </summary>
    /// <remarks>
    /// 忽略映射目標 ShipperDto 的 ShipperId
    /// 指定將 ShipperParameter.CompanyName 映射到 ShipperDto.CompanyName
    /// 指定將 ShipperParameter.Phone 映射到 ShipperDto.Phone
    /// </remarks>
    /// <param name="parameter"></param>
    /// <returns></returns>
    [MapperIgnoreTarget(nameof(ShipperDto.ShipperId))]
    [MapProperty(source: nameof(ShipperParameter.CompanyName), target: nameof(ShipperDto.CompanyName))]
    [MapProperty(source: nameof(ShipperParameter.Phone), target: nameof(ShipperDto.Phone))]
    public static partial ShipperDto MapToDto(ShipperParameter parameter);
}

接著修改 ShipperController,以下是修改後的程式碼

using Microsoft.AspNetCore.Mvc;
using Sample.Service.Interface;
using Sample.WebApplication.Infrastructure.Filters;
using Sample.WebApplication.Infrastructure.Mappers;
using Sample.WebApplication.Infrastructure.Wrapper.Models;
using Sample.WebApplication.Models.InputParameters;
using Sample.WebApplication.Models.OutputModels;

namespace Sample.WebApplication.Controllers;

/// <summary>
/// class ShipperController
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ShipperController : ControllerBase
{
    private readonly IShipperService _shipperService;

    /// <summary>
    /// Initializes a new instance of the <see cref="ShipperController"/> class
    /// </summary>
    /// <param name="shipperService">The shipperService</param>
    public ShipperController(IShipperService shipperService)
    {
        this._shipperService = shipperService;
    }

    //-----------------------------------------------------------------------------------------

    /// <summary>
    /// 取得所有 Shipper 資料
    /// </summary>
    /// <returns></returns>
    [HttpGet("all")]
    [Produces("application/json", "text/json")]
    [ProducesResponseType(200, Type = typeof(SuccessResultOutputModel<IEnumerable<ShipperOutputModel>>))]
    [ProducesResponseType(400, Type = typeof(FailureResultOutputModel<ResponseMessageOutputModel>))]
    public async Task<IActionResult> GetAllAsync()
    {
        // api/shipper/all.GET

        var shippers = await this._shipperService.GetAllAsync();
        var outputModels = shippers.Select(ShipperMapper.MapToOutputModel);
        return this.Ok(outputModels);
    }

    /// <summary>
    /// 取得指定範圍與數量的 Shipper 資料
    /// </summary>
    /// <param name="parameter"></param>
    /// <returns></returns>
    [HttpGet("from/{from}/size/{size}")]
    [ParameterValidator("parameter")]
    [Produces("application/json", "text/json")]
    [ProducesResponseType(200, Type = typeof(SuccessResultOutputModel<IEnumerable<ShipperOutputModel>>))]
    [ProducesResponseType(400, Type = typeof(FailureResultOutputModel<ResponseMessageOutputModel>))]
    public async Task<IActionResult> GetCollectionAsync([FromRoute] ShipperPageParameter parameter)
    {
        // api/shipper/{from}/{size}

        var totalCount = await this._shipperService.GetTotalCountAsync();
        if (totalCount == 0)
        {
            return this.Ok(Enumerable.Empty<ShipperOutputModel>());
        }

        var shippers = await this._shipperService.GetCollectionAsync(parameter.From, parameter.Size);
        var outputModels = shippers.Select(ShipperMapper.MapToOutputModel);

        return this.Ok(outputModels);
    }

    /// <summary>
    /// 輸入 companyName 或 phone 查詢相關的 shipper 資料
    /// </summary>
    /// <param name="parameter"></param>
    /// <returns></returns>
    [HttpGet("search")]
    [ParameterValidator("parameter")]
    [Produces("application/json", "text/json")]
    [ProducesResponseType(200, Type = typeof(SuccessResultOutputModel<IEnumerable<ShipperOutputModel>>))]
    [ProducesResponseType(400, Type = typeof(FailureResultOutputModel<ResponseMessageOutputModel>))]
    public async Task<IActionResult> SearchAsync([FromQuery] ShipperSearchParameter parameter)
    {
        // api/shipper/search.GET

        var shippers = await this._shipperService.SearchAsync(parameter.CompanyName, parameter.Phone);
        var outputModels = shippers.Select(ShipperMapper.MapToOutputModel);

        return this.Ok(outputModels);
    }

    /// <summary>
    /// 取得指定 ShipperId 的 Shipper 資料
    /// </summary>
    /// <param name="parameter">parameter</param>
    /// <returns></returns>
    [HttpGet]
    [Produces("application/json", "text/json")]
    [ParameterValidator("parameter")]
    [ProducesResponseType(200, Type = typeof(SuccessResultOutputModel<ShipperOutputModel>))]
    [ProducesResponseType(400, Type = typeof(FailureResultOutputModel<ResponseMessageOutputModel>))]
    public async Task<IActionResult> GetAsync([FromQuery] ShipperIdParameter parameter)
    {
        // api/shipper.GET

        var exists = await this._shipperService.IsExistsAsync(parameter.ShipperId);
        if (!exists)
        {
            return this.BadRequest(new ResponseMessageOutputModel { Message = "shipper not exists" });
        }

        var shipper = await this._shipperService.GetAsync(parameter.ShipperId);
        var outputModel = ShipperMapper.MapToOutputModel(shipper);
        return this.Ok(outputModel);
    }

    /// <summary>
    /// 新增 Shipper 資料
    /// </summary>
    /// <param name="parameter">parameter</param>
    /// <returns></returns>
    [HttpPost]
    [Consumes("application/json")]
    [Produces("application/json", "text/json")]
    [ParameterValidator("parameter")]
    [ProducesResponseType(200, Type = typeof(SuccessResultOutputModel<ShipperOutputModel>))]
    [ProducesResponseType(400, Type = typeof(FailureResultOutputModel<ResponseMessageOutputModel>))]
    public async Task<IActionResult> PostAsync([FromBody] ShipperParameter parameter)
    {
        // api/shipper.POST

        var shipper = ShipperMapper.MapToDto(parameter);

        var createResult = await this._shipperService.CreateAsync(shipper);
        if (!createResult.Success)
        {
            return this.BadRequest(new ResponseMessageOutputModel { Message = "create failure" });
        }

        return this.Ok(new ResponseMessageOutputModel { Message = "create success" });
    }

    /// <summary>
    /// 修改 Shipper 資料
    /// </summary>
    /// <param name="parameter">parameter</param>
    /// <returns></returns>
    [HttpPut]
    [Consumes("application/json")]
    [Produces("application/json", "text/json")]
    [ParameterValidator("parameter")]
    [ProducesResponseType(200, Type = typeof(SuccessResultOutputModel<ShipperOutputModel>))]
    [ProducesResponseType(400, Type = typeof(FailureResultOutputModel<ResponseMessageOutputModel>))]
    public async Task<IActionResult> PutAsync([FromBody] ShipperUpdateParameter parameter)
    {
        // api/shipper.PUT

        var exists = await this._shipperService.IsExistsAsync(parameter.ShipperId);
        if (!exists)
        {
            return this.BadRequest(new ResponseMessageOutputModel { Message = "shipper not exists" });
        }

        var shipper = await this._shipperService.GetAsync(parameter.ShipperId);

        shipper.CompanyName = parameter.CompanyName;
        shipper.Phone = parameter.Phone;

        var updateResult = await this._shipperService.UpdateAsync(shipper);
        if (!updateResult.Success)
        {
            return this.BadRequest(new ResponseMessageOutputModel { Message = "update failure" });
        }

        return this.Ok(new ResponseMessageOutputModel { Message = "update success" });
    }

    /// <summary>
    /// 刪除 Shipper 資料
    /// </summary>
    /// <param name="parameter">parameter</param>
    /// <returns></returns>
    [HttpDelete]
    [Consumes("application/json")]
    [Produces("application/json", "text/json")]
    [ParameterValidator("parameter")]
    [ProducesResponseType(200, Type = typeof(SuccessResultOutputModel<ShipperOutputModel>))]
    [ProducesResponseType(400, Type = typeof(FailureResultOutputModel<ResponseMessageOutputModel>))]
    public async Task<IActionResult> DeleteAsync([FromBody] ShipperIdParameter parameter)
    {
        // api/shipper.DELETE

        var exists = await this._shipperService.IsExistsAsync(parameter.ShipperId);
        if (!exists)
        {
            return this.BadRequest(new ResponseMessageOutputModel { Message = "shipper not exists" });
        }

        var deleteResult = await this._shipperService.DeleteAsync(parameter.ShipperId);
        if (deleteResult.Success)
        {
            return this.Ok(new ResponseMessageOutputModel { Message = "delete success" });
        }

        return this.BadRequest(new ResponseMessageOutputModel { Message = "delete failure" });
    }
}

執行 WebApplication 的單元測試以及服務的整合測試,驗證這樣的修改是讓應用程式可以正確地執行

最後就是將 Mapster 從 Solution 裡的各個專案裡移除,最後再重新把所有測試專案執行一遍,全部通過

以上就完成了 Mapperly 的應用與實作,並完全移除了 Mapster 相關的套件和程式碼。

對了,使用 Mapperly 並不需要額外進行 DI 注入設定。這主要因為 Mapperly 是利用 Source Genarator 在編譯階段產生靜態映射方法,這些方法可以直接以靜態方式呼叫,而不需要透過依賴注入的方式注入到應用程式中。

另外單元測試專案裡也不需要特別為 Mapperly 做額外的設定。由於 Mapperly 生成的是靜態方法,可以直接在測試案例中呼叫映射方法,而不需要透過依賴注入來取得或管理映射器。因此,在單元測試專案中,可以直接測試映射器生成的功能,而不用另外設定 Mapperly 至 DI 容器。這使得測試流程更加簡單和直接。

 

查看 Mapperly 所生成的程式碼

以我所使用的 Rider 為例,我可以在方案總管裡的 Sample.Service 專案下開啟Dependencies > .NET 8.0 > Source Generators然後再展開Riok.Mapperly.MapperGenerator就可以看到ShipperMapper.g.cs,將檔案點開就可以查看程式碼

Sample.Service - ShipperMapper.g.cs

// <auto-generated />
#nullable enable
namespace Sample.Service.Mappers
{
    public static partial class ShipperMapper
    {
        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.2.0.0")]
        [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(dto))]
        public static partial global::Sample.Domain.Entities.ShipperModel? MapToModel(global::Sample.Service.Dto.ShipperDto? dto)
        {
            if (dto == null)
                return default;
            var target = new global::Sample.Domain.Entities.ShipperModel();
            target.ShipperId = dto.ShipperId;
            target.CompanyName = dto.CompanyName;
            target.Phone = dto.Phone;
            return target;
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.2.0.0")]
        [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(model))]
        public static partial global::Sample.Service.Dto.ShipperDto? MapToDto(global::Sample.Domain.Entities.ShipperModel? model)
        {
            if (model == null)
                return default;
            var target = new global::Sample.Service.Dto.ShipperDto();
            target.ShipperId = model.ShipperId;
            target.CompanyName = model.CompanyName;
            target.Phone = model.Phone;
            return target;
        }
    }
}

再來看看另一個 WebApplication  所生成的程式碼

Sample.WebApplication - ShipperMapper.g.cs

// <auto-generated />
#nullable enable
namespace Sample.WebApplication.Infrastructure.Mappers
{
    public static partial class ShipperMapper
    {
        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.2.0.0")]
        [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(dto))]
        public static partial global::Sample.WebApplication.Models.OutputModels.ShipperOutputModel? MapToOutputModel(global::Sample.Service.Dto.ShipperDto? dto)
        {
            if (dto == null)
                return default;
            var target = new global::Sample.WebApplication.Models.OutputModels.ShipperOutputModel();
            target.ShipperId = dto.ShipperId;
            target.CompanyName = dto.CompanyName;
            target.Phone = dto.Phone;
            return target;
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.2.0.0")]
        [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(nameof(parameter))]
        public static partial global::Sample.Service.Dto.ShipperDto? MapToDto(global::Sample.WebApplication.Models.InputParameters.ShipperParameter? parameter)
        {
            if (parameter == null)
                return default;
            var target = new global::Sample.Service.Dto.ShipperDto();
            target.CompanyName = parameter.CompanyName;
            target.Phone = parameter.Phone;
            return target;
        }
    }
}

 

根據 Mapperly 官方文件

你可以透過以下方法來查看 Mapperly 所生成的程式碼:

  1. 編譯後檢查 obj 目錄:Code Genarator 會將自動產生的程式碼寫入到 obj 目錄底下,檔名通常包含 .g.cs 字樣。你可以在該目錄中搜尋類似 YourProject.Mapperly.g.cs 或其他具有 Mapperly 標記的檔案。打開這些檔案後,你可以檢視 Mapperly 生成的映射程式碼,了解每個映射方法的具體實作。
  2. 使用 IDE 的內建功能:在 Visual Studio 中,可以將滑鼠游標停留在 Mapperly 生成的靜態映射方法上(例如 ShipperMapper.MapToDto(parameter)),右鍵選擇「移至定義」,這樣 IDE 會直接打開生成的程式碼檔案,方便你瀏覽檢查。
  3. 啟用生成檔案輸出設定(選擇性):如果希望每次編譯時都能將生成的程式碼存放在一個指定的資料夾中,方便持續檢查與除錯,可以在你的專案檔案(.csproj)中加入下列設定:

開啟 Service 與 WebApplication 專案的 csproj 檔案,並在檔案裡加入

<PropertyGroup>
  <!-- 啟用詳細的源生成器診斷資訊 -->
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

預設生成的原始碼檔案路徑會在專案目錄的obj/Debug/net8.0

在我的 Service 專案裡,生成的程式碼就會在Sample.Service\obj\Debug\net8.0\generated\Riok.Mapperly\Riok.Mapperly.MapperGenerator路徑的資料夾裡

預設生成的路徑是$(BaseIntermediateOutputPath)Generated,BaseIntermediateOutputPath  是 MSBuild 本身提供的屬性,用於指定編譯過程中中介輸出檔案(例如生成的檔案)的目錄。也就是說,各種 Source Ganarator(包括 Mapperly)在生成輸出檔案時,通常會使用這個變數來定位和存放這些檔案,而它屬於 .NET SDK 和 MSBuild 的預定義屬性。

如果你不想讓程式碼藏得這麼深的話,也可以做調整讓生成的程式碼存放在一個指定的資料夾中,例如將設定修改為以下的內容

<PropertyGroup>
  <!-- 啟用將編譯器產生的檔案輸出 -->
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

那麼生成的程式碼存放在Sample.Service\Generated\資料夾下

我不建議直接產生在專案根目錄下,因為這會在方案總管裡看到

如果是以下路徑設定$(BaseIntermediateOutputPath)Generated就會直接產生在Sample.Service\obj\資料夾下

<PropertyGroup>
	<!-- 啟用詳細的源生成器診斷資訊 -->
	<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
	<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

不過我是覺得直接使用預設的路徑就可以了,或者說產生檔案並不重要,那麼以上 csproj  設定的修改就可以完全忽略,在 IDE 不管是 Rider 或 Visual Studio 2022 裡都是可以看到的。

Visual Studio 2022

專案在經過編譯後,在 ShipperMapper 裡,在 ShipperMapper 類別的 MapToModel 方法點擊滑鼠右鍵,然後點選「移至定義」

就會自動開啟ShipperMapper.g.cs的程式碼,不過這份程式碼在沒有對 csproj 內容做設定的情況下,這個程式碼檔案只是能夠看而已

在方案總管裡,展開Sample.Service >  相依性 > 分析器 > Riok Mapperly > Riok.Mapperly.MapperGenerator,就可以看到ShipperMapper.g.cs檔案

最後

這一篇是對 Mapperly 的應用與操作做個簡單的介紹,其實還有很多映射設定還需要再仔細去做研究。

如同一開始我說過的,對於以往我們所習慣的映射工具如:AutoMapper, Mapster,這兩種我會看做是同一類型的,同質性比較高,設定、使用習慣與觀念上是比較相近。而 Mapperly 雖然也是歸屬於映射工具的一種,但我會把它與 AutoMapper, Mapster 視為不同的映射工具。

Mapster 與 AutoMapper 的相似之處

  • 使用方式與配置模式接近
    Mapster 在大部分情境下提供的使用方式與 AutoMapper 非常相似:
    • 可以透過建立配置(例如透過 IRegister 接口或全域設定)來定義來源與目標之間的映射規則。
    • 兩者都支援類似的自動掃描組件與屬性匹配,讓你的轉換代碼可以盡量少寫樣板程式碼。
  • 替換的門檻低
    因為兩者操作邏輯和設計思路較接近,所以對於習慣使用 AutoMapper 的團隊,轉換到 Mapster 通常更容易上手。
    • Mapster 除了具備相似的配置方式,還在效能上有明顯的優勢,這使得它在一些專案中成為理想的替代方案。

Mapperly 的不同角度

  • Source Ganarator 驅動
    Mapperly 採用 C# Source Genarator 來產生映射代碼,這意味著它在映射過程中完全在編譯時生成靜態代碼,而不是在運行時期依賴反射或 Expression Tree。
    • Mapperly 提供的映射往往有更高的執行效能,並且可以在編譯期捕捉到更多錯誤,但其使用方式和配置風格與 AutoMapper 或 Mapster 有顯著不同。
  • 配置方式更顯式
    由於 Mapperly 的映射配置通常需要透過 Attribute 來定義映射規則,這使得映射邏輯看起來更顯式,但也表示在一開始要上手是有一定的學習曲線。
    • 對於那些希望完全在編譯時期解決映射問題,且願意接受一套全新語法與模式的團隊,Mapperly 會是一個不錯的選擇。

總結來說,Mapster 的配置與使用方式與 AutoMapper 相似,非常適合想要低成本轉換而又要求高效能的專案;而 Mapperly 則提供另一種解決方案,強調編譯時期生成靜態映射代碼、效能與錯誤檢查,適合對映射要求更嚴苛、願意投入學習新工具的團隊。最終的選擇應依據專案需求、性能考量以及團隊的技術熟悉程度來決定。

 

相關連結

Mapperly

相關影片:

相關文章:

以上

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力