在 .NET
项目开发中,实体模型(Entity)、数据传输对象(DTO)
与 前端视图模型(ViewModel)
之间的转换几乎是家常便饭。手写 new +
逐个字段赋值不仅繁琐,还容易遗漏。此时,一款轻量、高性能的对象映射库就显得尤为重要。
今天介绍的是 Mapster,它比 AutoMapper 更轻量、编译时性能更高,并且支持丰富的映射策略。下面以 SwitchData 项目中的实际用法为例,带你从基础配置走到复杂树形结构转换。
一、为什么选择 Mapster?
Mapster 的核心优势可以概括为三点:
- 高性能:基于表达式树编译,运行时性能接近手写代码。
- 零依赖:单个 NuGet 包即可工作,不需要额外的 DI 扩展包(虽然也有)。
- 配置灵活:支持
Map、AfterMapping、MapWith、Ignore等多种映射方式,复杂场景也能覆盖。
在 SwitchData.Api 的 Program.cs
中,对象映射的初始化只有一行:
// 6. 对象映射
MapsterConfig.Configure();
所有的映射规则都被收敛到 MapsterConfig
静态类中,便于集中管理和维护。
二、集中式配置:一个类管理所有映射
SwitchData 采用了集中式配置的方式。通过
TypeAdapterConfig<TSource, TDestination>.NewConfig()
定义源类型到目标类型的映射规则。
2.1 简单字段同名映射
当源类型和目标类型的字段名称、类型基本一致时,Mapster 可以自动完成映射:
// User -> UserDto
TypeAdapterConfig<User, UserDto>.NewConfig();
调用时只需要一行:
var userDto = user.Adapt<UserDto>();
2.2 字段类型不一致时的显式映射
在实际业务中,数据库常以 int 存储布尔标志位(0 和
1),而 DTO 层希望直接使用 bool。Mapster 的
Map 方法可以轻松处理这种转换:
// MenuDto -> VueMenu:数据库 int 转 bool
TypeAdapterConfig<MenuDto, VueMenu>
.NewConfig()
.Map(dest => dest.IsCache, src => src.IsCache > 0)
.Map(dest => dest.IsDelete, src => src.IsDelete > 0)
.Map(dest => dest.IsFrame, src => src.IsFrame > 0)
.Map(dest => dest.Visible, src => src.Visible > 0);
反过来,bool 转 int 也同样简单:
// VueMenu -> MenuDto:bool 转 int
TypeAdapterConfig<VueMenu, MenuDto>
.NewConfig()
.Map(dest => dest.IsCache, src => src.IsCache ? 1 : 0)
.Map(dest => dest.IsDelete, src => src.IsDelete ? 1 : 0)
.Map(dest => dest.IsFrame, src => src.IsFrame ? 1 : 0)
.Map(dest => dest.Visible, src => src.Visible ? 1 : 0);
这种方式比手写 if 判断要清晰得多,也更容易维护。
三、AfterMapping:映射完成后的二次加工
有些字段无法通过简单的名称对应或表达式完成,例如需要初始化集合、拼接字符串,或者组合多个源字段生成目标字段。这时可以使用
AfterMapping。
在 SwitchData 中,Vue 前端需要的菜单树 MenuTree
与后端实体 VueMenu 结构差异较大:
TypeAdapterConfig<VueMenu, MenuTree>
.NewConfig()
.AfterMapping((src, dest) =>
{
dest.children = new List<MenuTree>();
dest.component = src.Component.ToString();
dest.hidden = !src.Visible;
dest.meta = new MenuMeta
{
icon = src.Icon,
link = src.IsFrame ? src.Path : null,
title = src.Text,
noCache = !src.IsCache
};
dest.name = src.Path.ToString();
dest.id = src.Id.ToString();
dest.pid = src.ParentId.ToString();
dest.alwaysShow = src.MenuType == "M";
dest.path = src.Path;
});
这里的关键点:
children必须初始化为空列表,否则后续构建树形结构时会报空引用异常。hidden、noCache等字段是语义反转的,需要在映射时取反。meta是嵌套对象,需要单独构造。
AfterMapping
相当于映射完成后的”收尾工作”,非常适合处理这种结构差异大的场景。
四、MapWith:完全自定义的映射逻辑
当源集合和目标结构根本不在同一个维度上时,例如把数据库查询出来的扁平列表转换成前端需要的多级树形结构,简单的字段映射已经不够用了。Mapster
提供了 MapWith 来自定义整个转换逻辑。
在 SwitchData 中,NeTypeView
是一个扁平列表,每个记录包含网络类型、厂商、设备类型等信息。前端组件需要的是一棵树:
graph TD
A[网络类型] --> B[厂商]
B --> C[设备类型]
Mapster 配置如下:
TypeAdapterConfig<List<NeTypeView>, List<NeTreeDto>>
.NewConfig()
.MapWith(dtos => BuildTree(dtos));
BuildTree 方法负责把扁平数据分层组装:
public static List<NeTreeDto> BuildTree(IEnumerable<NeTypeView> dtos)
{
var tree = new List<NeTreeDto>();
// 第一层:按网络类型分组
var networkTypes = dtos.Select(s => new
{
NetworkId = s.NetworkTypeId,
NetworkName = s.NetworkTypeName,
NetworkAbbr = s.NetworkTypeAbbreviation,
}).Distinct();
foreach (var gnt in networkTypes)
{
var gntNode = new NeTreeDto
{
Id = gnt.NetworkId,
Label = gnt.NetworkName,
Route = gnt.NetworkName,
Children = new List<NeTreeDto>(),
Type = 0
};
// 第二层:按厂商分组
var manufacturers = dtos
.Where(s => s.NetworkTypeId == gnt.NetworkId)
.Select(s => new { s.VendorId, s.VendorName, s.VendorAbbreviation })
.Distinct();
foreach (var em in manufacturers)
{
var emNode = new NeTreeDto
{
Id = em.VendorId,
Label = em.VendorName,
Route = $"{gnt.NetworkName}/{em.VendorName}",
Children = new List<NeTreeDto>(),
Type = 0
};
// 第三层:设备类型
var deviceTypes = dtos
.Where(s => s.NetworkTypeId == gnt.NetworkId && s.VendorId == em.VendorId)
.Select(s => new { s.DeviceTypeId, s.DeviceTypeName, s.DeviceTypeAbbreviation })
.Distinct();
foreach (var get in deviceTypes)
{
emNode.Children.Add(new NeTreeDto
{
Id = get.DeviceTypeId,
Label = get.DeviceTypeName,
Route = $"{gnt.NetworkName}/{em.VendorName}/{get.DeviceTypeName}",
Type = 1
});
}
gntNode.Children.Add(emNode);
}
tree.Add(gntNode);
}
return tree;
}
使用时依然是一行调用:
var tree = neTypeViews.Adapt<List<NeTreeDto>>();
通过
MapWith,我们把”映射规则”和”业务组装逻辑”解耦:Mapster
负责触发映射,而复杂的树形构建交给专门的业务方法处理。
五、使用建议与注意事项
- 配置尽早执行:
MapsterConfig.Configure()应在应用启动时调用一次,确保所有映射规则在第一次Adapt前已注册。 - 双向映射分别配置:
A -> B和B -> A的映射规则需要分别声明,Mapster 不会自动推导反向映射。 - 避免在 AfterMapping
中做重型计算:后处理适合字段赋值和初始化,复杂计算建议用
MapWith或外部服务。 - 善用
Adapt扩展方法:只要引用了Mapster命名空间,任何对象都可以直接调用.Adapt<T>(),非常自然。
六、总结
Mapster 是一款值得在 .NET
项目中认真考虑的映射工具。从简单的同名字段映射,到 int/bool
类型转换,再到 AfterMapping 后处理,以及
MapWith 自定义复杂映射,它都能胜任。
在 SwitchData 项目中,菜单模型转换和网元类型树构建都得益于 Mapster 的灵活配置。如果你的项目还在为 DTO/实体/视图模型之间的转换写大量样板代码,不妨试试 Mapster。