在 .NET 项目中,对象映射是层间数据传递的日常操作。AutoMapper 久负盛名,但 Mapster 以更轻量、更快的性能和更灵活的 API 赢得了不少开发者的青睐。本文从 SwitchData 项目中提取三个真实场景,深入讲解 Mapster 的 AfterMapping 后映射钩子、Map 条件映射和 MapWith 自定义映射函数,最后展示如何将扁平数据一键转为树形结构。
一、Mapster 简介
Mapster 是一个高性能的 .NET 对象映射库,核心特点:
- 编译时生成映射代码,性能接近手写赋值
- 链式 API,配置直观易读
- 支持 AfterMapping、MapWith、Include 等高级特性
- 支持双向映射和自定义转换规则
<!-- NuGet 安装 -->
<PackageReference Include="Mapster" Version="7.4.0" />
二、AfterMapping:映射后的钩子操作
场景说明
在 SwitchData 中,VueMenu
是数据库实体,MenuTree
是前端路由树节点。两者字段名和类型差异较大:MenuTree
需要嵌套的 meta 对象、children 列表,以及从
bool 到显示逻辑的转换。这种复杂的属性组装,正是
AfterMapping 的用武之地。
代码实现
// VueMenu -> MenuTree:数据库实体转前端路由树
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;
});
要点解析
- AfterMapping 在默认映射之后执行,可以覆盖已映射的值,也可以补充默认映射无法处理的属性
meta是嵌套对象,Mapster 默认不会自动创建,在 AfterMapping 中手动实例化并赋值hidden = !src.Visible、noCache = !src.IsCache这种取反逻辑,在 AfterMapping 中一目了然link = src.IsFrame ? src.Path : null条件赋值,比.Map()更适合复杂分支
反向映射同理
// MenuTree -> VueMenu:前端路由树转回数据库实体
TypeAdapterConfig<MenuTree, VueMenu>
.NewConfig()
.AfterMapping((src, dest) =>
{
dest.Id = Convert.ToInt32(src.id);
dest.ParentId = Convert.ToInt32(src.pid);
dest.Component = src.component;
dest.Visible = !src.hidden;
dest.Icon = src.meta.icon;
dest.IsFrame = src.meta.link != null;
dest.Text = src.meta.title;
dest.IsCache = !src.meta.noCache;
dest.Path = src.path;
dest.MenuType = src.MenuType;
dest.IsDelete = false;
});
AfterMapping 的本质:当源和目标的属性不是简单的同名对应,或者需要额外的逻辑处理时,AfterMapping 提供了一个”映射后干预点”,让你在默认映射结果上做二次加工。
三、Map 条件映射:类型转换的一对一映射
场景说明
MenuDto 继承自 VueMenu,但数据库中
IsCache、IsDelete、IsFrame、Visible
是 int 类型(0/1),而 VueMenu 中是
bool。Mapster 默认不会做 int <-> bool
转换,需要用 .Map() 显式指定。
正向映射:int -> bool
// 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);
要点解析
.Map(dest, src)是 Mapster 最基础的自定义映射,适合一对一的属性转换- 当源类型和目标类型不兼容(如
int <-> bool),必须显式.Map() - 链式调用
.Map().Map().Map()让配置紧凑且可读 - 正反两个方向的映射要分别配置,Mapster 不会自动推断反向规则
Map vs AfterMapping 的选择:如果只是单个属性的简单转换,用
.Map();如果涉及多属性组合、嵌套对象创建,用 AfterMapping。
四、MapWith + BuildTree:扁平数据一键转树
场景说明
NeTypeView
是数据库视图返回的扁平记录,每行包含网络类型、厂商、设备类型、网元类型四级信息。前端需要的是树形结构:网络类型
-> 厂商 -> 设备类型,逐级嵌套。Mapster 的 MapWith
可以直接指定整个映射函数。
MapWith 配置
// 将扁平列表映射为树形列表
TypeAdapterConfig<List<NeTypeView>, List<NeTreeDto>>
.NewConfig()
.MapWith(dtos => BuildTree(dtos));
一行配置,将 List<NeTypeView> 到
List<NeTreeDto> 的映射委托给 BuildTree
方法。
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,
Abbrev = gnt.NetworkAbbr,
Children = new List<NeTreeDto>(),
AbbrevGroup = gnt.NetworkAbbr,
Type = 0,
};
// 第二层:当前网络类型下的厂商
var manufacturers = dtos
.Where(s => s.NetworkTypeId == gnt.NetworkId)
.Select(s => new
{
VendorId = s.VendorId,
VendorName = s.VendorName,
VendorAddr = s.VendorAbbreviation
}).Distinct();
foreach (var em in manufacturers)
{
var emNode = new NeTreeDto
{
Id = em.VendorId,
Label = em.VendorName,
Route = $"{gntNode.Label}/{em.VendorName}",
Abbrev = em.VendorAddr,
Children = new List<NeTreeDto>(),
AbbrevGroup = $"{gnt.NetworkAbbr}_{em.VendorAddr}",
Type = 0
};
// 第三层:当前厂商下的设备类型
var deviceTypes = dtos
.Where(s => s.NetworkTypeId == gnt.NetworkId
&& s.VendorId == em.VendorId)
.Select(s => new
{
s.DeviceTypeId,
s.DeviceTypeName,
DeviceTypeAbbr = s.DeviceTypeAbbreviation
}).Distinct();
foreach (var get in deviceTypes)
{
var getNode = new NeTreeDto
{
Id = get.DeviceTypeId,
Label = get.DeviceTypeName,
Route = $"{gntNode.Label}/{emNode.Label}/{get.DeviceTypeName}",
Abbrev = get.DeviceTypeAbbr,
AbbrevGroup = $"{gnt.NetworkAbbr}_{em.VendorAddr}_{get.DeviceTypeAbbr}",
Children = new List<NeTreeDto>(),
Type = 1,
};
emNode.Children.Add(getNode);
}
gntNode.Children.Add(emNode);
}
tree.Add(gntNode);
}
return tree;
}
要点解析
- MapWith 绕过默认映射引擎,直接执行自定义函数,适合整体结构变化的场景
- 三层嵌套循环 + LINQ 分组去重:用
Distinct()对匿名类型去重,避免重复节点 - Route 路径累积:子节点 Route 拼接父节点路径,如
5GC/华为/AAU - AbbrevGroup 编码累积:用下划线连接各级缩写,如
5GC_HW_AAU,用于前端快速定位 - Type 标记叶子节点:
Type = 0表示分支,Type = 1表示叶子,前端据此渲染不同图标
五、映射配置的注册与使用
在启动时注册
// Program.cs 或 Startup.cs
MapsterConfig.Configure();
使用映射
// 简单映射
var menuTree = vueMenu.Adapt<MenuTree>();
// 列表映射(走 MapWith 配置)
var tree = neTypeViews.Adapt<List<NeTreeDto>>();
// 反向映射
var vueMenu = menuTree.Adapt<VueMenu>();
.Adapt<T>() 是 Mapster 的扩展方法,根据已注册的
TypeAdapterConfig 自动选择映射策略。
六、三种映射方式对比
| 特性 | .Map() |
AfterMapping |
MapWith |
|---|---|---|---|
| 适用场景 | 单属性类型转换 | 多属性组合赋值、嵌套对象 | 整体结构变换 |
| 执行时机 | 映射过程中 | 默认映射之后 | 替代默认映射 |
| 性能 | 编译时生成,最快 | 默认映射 + 钩子,次之 | 自定义函数,取决于实现 |
| 配置复杂度 | 低 | 中 | 高 |
| 典型用例 | bool <-> int |
创建嵌套对象 | 扁平转树形 |
七、总结
Mapster 的三种高级映射特性各有定位:
- Map:处理属性级的类型不匹配,一行配置搞定
int <-> bool转换 - AfterMapping:处理对象级的后置逻辑,适合创建嵌套对象、组合赋值、条件分支
- MapWith:处理结构级的整体变换,将扁平列表一键转为树形结构
在实际项目中,三者常常组合使用:Map
处理简单类型转换,AfterMapping
处理复杂属性组装,MapWith 处理结构性变换。SwitchData
项目正是这种组合的典型实践——菜单管理用
Map + AfterMapping,网元管理用
MapWith + BuildTree,各取所长,代码清晰且高性能。