Mapperly 在以前找寻替代 AutoMapper 的时候就有看过,但当时着重在与 AutoMapper 设定与操作习惯相近的替代套件,所以对于 Mapperly 就没有太多的关注。
直到写了「替换映射工具 - 使用 Mapster」这篇文章后才稍微去看看 Mapperly,发现到它和 AutoMapper, Mapster 虽然都是属于 Mapping 工具,都是做物件对映转换的处理,但设定与使用上就有蛮大的差别,所以写篇文章做个简单的纪录。
.NET Object Mappers Benchmark
在写「替换映射工具 - 使用 Mapster」这篇文章的最后有引用参考了「.NET Object Mappers Benchmark」的资料,对于几种主要常见的 Mapping 工具跑了 benchmark 做效能的比对,物件转换的工具有:
- Mapperly
- Mapster
- AutoMapper
- EmitMapper
- TinyMapper
- ExpressMapper
- AgileMapper
- Manul mapping - Generated by MappingGenerator (这是个 Visual Studio Extension)
执行结果:
- 最快的是 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
- https://github.com/riok/mapperly
- https://mapperly.riok.app/
- https://mapperly.riok.app/docs/intro/
由于 Mapperly 在建置时会建立映射程式码,因此运行时的开销很小。尤其是,生成的程式码具有完美的可读性,使您可以轻鬆验证生成的映射程式码。
相关的影片介绍:
- The Best .NET Mapper to Use in 2023 | Nick Chapasa
- Mapperly - .NET object mapping like a boss | Mohamad Dbouk
相关文章介绍:
- Mapperly: The Coolest Object Mapping Tool in Town - Mohamad Dbouk
- Efficient Object Mapping with Mapperly in .NET | Oleg Kyrylchuk
- Mapperly:高效对象映射源码生成工具-CSDN博客
- 推荐使用Mapperly:高效对象映射的源码生成器-CSDN博客
- C# 对象映射框架(Mapster & mapperly) - J.晒太阳的猫 - 博客园
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
- https://mapperly.riok.app/docs/configuration/flattening/
在 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 - Generated source
你可以透过以下方法来查看 Mapperly 所生成的程式码:
- 编译后检查 obj 目录:Code Genarator 会将自动产生的程式码写入到
obj
目录底下,档名通常包含.g.cs
字样。你可以在该目录中搜寻类似YourProject.Mapperly.g.cs
或其他具有 Mapperly 标记的档案。打开这些档案后,你可以检视 Mapperly 生成的映射程式码,了解每个映射方法的具体实作。 - 使用 IDE 的内建功能:在 Visual Studio 中,可以将滑鼠游标停留在 Mapperly 生成的静态映射方法上(例如 ShipperMapper.MapToDto(parameter)),右键选择「移至定义」,这样 IDE 会直接打开生成的程式码档案,方便你浏览检查。
- 启用生成档案输出设定(选择性):如果希望每次编译时都能将生成的程式码存放在一个指定的资料夹中,方便持续检查与除错,可以在你的专案档案(.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 视为不同的映射工具。
Mapperly 通过利用 .NET 的 Source Generator 功能,提供了一个高效、可读且易于 Debug 的映射解决方案。对于追求高效能和较低运行时负担的应用而言,这是一个值得考虑的替代方案。开发者可以根据具体的应用需求和团队习惯,决定是否要从 AutoMapper 或 Mapster 切换到 Mapperly。
这样的设计概念也反映了目前 .NET 开发中对于编译时期安全性与运行时期效能平衡的追求。如果你对 Mapperly 感兴趣,不妨深入阅读其官方文件,以了解更多细节和配置示例。
相关连结
Mapperly
- https://github.com/riok/mapperly
- https://mapperly.riok.app/
- https://mapperly.riok.app/docs/intro/
相关影片:
- The Best .NET Mapper to Use in 2023 | Nick Chapasa
- Mapperly - .NET object mapping like a boss | Mohamad Dbouk
相关文章:
- Mapperly: The Coolest Object Mapping Tool in Town - Mohamad Dbouk
- Efficient Object Mapping with Mapperly in .NET | Oleg Kyrylchuk
- Mapperly:高效对象映射源码生成工具-CSDN博客
- 推荐使用Mapperly:高效对象映射的源码生成器-CSDN博客
- C# 对象映射框架(Mapster & mapperly) - J.晒太阳的猫 - 博客园
以上
纯粹是在写兴趣的,用写程式、写文章来抒解工作压力