Mapster 对象映射实战:比 AutoMapper 更快、更轻量的 C# 映射方案

在日常的 .NET 开发里,“实体转 DTO、DTO 转实体、VO 转 PO” 这类对象映射代码写起来枯燥又容易出错。传统做法不外乎手动 new 一个个属性赋值,或者用 AutoMapper 这类重型库。前者维护困难,后者虽然功能强大,但反射开销和配置复杂度往往让人望而却步。

最近在维护 SwitchData 项目时,我发现代码里大量使用了 Mapster 来处理菜单、用户、网元等模块的对象转换。相比 AutoMapper,Mapster 的编译时映射生成机制让它在性能和易用性之间取得了很好的平衡。本文就结合项目源码,聊一聊 Mapster 的核心用法和实战技巧。

一、Mapster 是什么

Mapster 是一个高性能、轻量级的 .NET 对象映射库,支持:

  • 编译时映射:默认生成基于表达式的映射代码,运行时无需反射,速度接近手写代码。
  • 灵活配置:支持简单字段映射、自定义转换、嵌套对象、集合映射、双向映射等。
  • 与现有代码共存:既可以全局配置 TypeAdapterConfig,也可以局部用 .Adapt<T>() 直接映射。

对于中小型项目或者对性能敏感的场景,Mapster 是一个很好的选择。

二、在项目中初始化 Mapster

Mapster 的配置通常放在程序启动时一次性注册。在 SwitchData.Api 的 Program.cs 中,可以看到这样一行:

// 6. 对象映射
MapsterConfig.Configure();

对应的 MapsterConfig.cs 是一个静态类,里面集中管理了所有映射规则:

public static class MapsterConfig
{
    public static void Configure()
    {
        // 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;
            });
    }
}

这种方式的好处是:映射规则集中、便于维护,新业务上线时只需要来这里加一段配置即可。

三、基础映射:同名属性自动转换

最常用也最简单的是同名属性自动映射。例如 AccountService.cs 中新增用户时:

var entity = dto.Adapt<User>();

UserDtoUser 的属性名、类型一致时,Mapster 会自动完成赋值,代码非常清爽。

如果目标对象已经存在,还可以映射到现有实例,避免重复创建对象:

var user = UserData.GetUserObject(dto.UserId);
var entity = dto.Adapt(user);  // 把 dto 映射到已有的 user 对象

四、自定义映射:字段名不同或类型转换

实际业务里,源对象和目标对象的字段往往不会完全对应。例如菜单模块中,数据库用 int 表示布尔值(0/1),而前端需要 bool

// MenuDto -> VueMenu
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);

Map(...) 方法可以接收一个表达式,把源字段按需转换后再赋值。反过来,从实体转 DTO 时也可以把 bool 转成 int

// VueMenu -> MenuDto
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);

五、映射后处理:AfterMapping

有些字段无法通过简单表达式完成,需要在映射结束后做额外处理。Mapster 提供了 AfterMapping

TypeAdapterConfig<VueMenu, MenuTree>
    .NewConfig()
    .AfterMapping((src, dest) =>
    {
        dest.children = new List<MenuTree>();
        dest.meta = new MenuMeta
        {
            title = src.Text,
            icon = src.Icon,
            noCache = !src.IsCache
        };
        dest.hidden = !src.Visible;
    });

MenuService.cs 中,构建菜单树时大量使用了这种映射方式:

var menuDict = webMenus.ToDictionary(
    s => s.Id.ToString(),
    s => s.Adapt<MenuTree>());

每个 VueMenu 对象会按照上面的规则映射成前端需要的 MenuTree,包括初始化子节点集合、封装 meta 对象等。

六、自定义整个对象的构建:MapWith

当源集合和目标结构差异较大时,可以用 MapWith 指定一个自定义的构建方法。例如:

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,
    }).Distinct();

    foreach (var gnt in networkTypes)
    {
        var gntNode = new NeTreeDto { ... };
        var manufacturers = dtos.Where(s => s.NetworkTypeId == gnt.NetworkId).Distinct();
        foreach (var em in manufacturers)
        {
            var emNode = new NeTreeDto { ... };
            gntNode.Children.Add(emNode);
        }
        tree.Add(gntNode);
    }
    return tree;
}

调用方只需要一行代码:

var trees = viewDtos.Adapt<List<NeTreeDto>>();

复杂的树形构建逻辑被封装在配置里,业务代码保持简洁。

七、Mapster 与 AutoMapper 的对比

特性 Mapster AutoMapper
映射方式 默认编译时生成 运行时反射
性能 更高 相对较低
包体积 更轻量 较大
配置风格 链式 Fluent API Profile + CreateMap
学习曲线 平缓 较陡峭
反向映射 支持 TwoWays() 支持 ReverseMap()

如果你的项目只是做常见的 DTO/Entity 转换,Mapster 通常足够用,而且性能优势明显。

八、使用建议与注意事项

  1. 配置集中管理:像 SwitchData 这样把所有映射规则放在 MapsterConfig 中,便于查找和维护。
  2. 避免在热路径里重复配置TypeAdapterConfig 的配置应该在启动时完成,不要每次请求都注册。
  3. 注意嵌套映射:Mapster 会自动处理嵌套对象,但如果嵌套类型也需要自定义规则,记得先注册外层再注册内层。
  4. 善用 AfterMapping:初始化集合、设置默认值、计算派生字段都可以放在这里。
  5. 调试时生成映射代码:Mapster 支持 TypeAdapterConfig.GlobalSettings.Compiler = CompilerType.Expression; 等选项,必要时可以查看生成的表达式树。

九、总结

Mapster 是一个值得在 .NET 项目中尝试的对象映射库。它既能减少样板代码,又能通过编译时映射保证性能。在 SwitchData 项目中,从菜单树构建到用户信息转换,Mapster 的配置式用法让代码结构清晰了很多。

如果你的项目还在手写 new Dto { A = entity.A, B = entity.B },不妨试试 Mapster,把重复劳动交给库,把精力放在业务逻辑上。