跳转至

WPF 项目用显式映射替代 Mapperly

  • 归档日期:2026-08-25
  • 适用基线:.NET 10 LTS + C# 14 + WPF + CommunityToolkit.Mvvm
  • 决策状态:采用;取代本项目对 Mapperly 的推荐

DTO、领域模型和 ViewModel 三者之间的完整双向流程、项目位置与代码示例,见 DTO、领域模型与 ViewModel 映射闭环

最终结论

本项目不再使用 Mapperly 直接映射 ViewModel,也不换成另一个会自动写入 ViewModel 属性的映射器。推荐闭环改为:

DTO / 持久化模型
        ├─ 普通 C# 强类型扩展方法(机械的数据边界转换)
Application / Domain Model
        ├─ ViewModel 构造函数或按用例命名的 Factory
        │  在构造期间直接给 ObservableProperty backing field 赋值
WPF ViewModel
        └─ ToCommand / ToRequest / Apply 等显式方法输出用户修改

配套增效方式:让 Visual Studio/GitHub Copilot 生成映射方法草稿,然后把普通 .cs 源码提交仓库、审查并测试。它只负责减少敲代码,不成为构建期或运行时依赖。

为什么不是简单换成 AutoMapper 或 Mapster

运行时映射器是在 CommunityToolkit.Mvvm 生成属性之后执行,因此能绕过“一个源码生成器看不到另一个生成器输出”的问题。但它映射到 ViewModel 时仍会调用公共 setter,通知、验证、OnXxxChanged、Command 联动和 Messenger 等副作用并不会消失。

因此:

  • 换成 AutoMapper:避开生成器排序,但没有解决 ViewModel setter 的语义问题。
  • 换成 Mapster 运行时映射:同样能看到最终属性,但写入已有对象或新对象时仍属于属性映射;配置还必须额外做 fail-fast 校验。
  • 换成另一款源码生成 Mapper:仍可能遇到生成器链、工具版本或成员发现边界。
  • 显式构造函数/Factory:可以在初始化阶段直接写 backing field,明确区分“加载初始数据”和“用户修改属性”。

Mapster 官方同时提供运行时 Adapt、映射到已有对象和代码生成能力;其配置文档建议通过 Compile() 以及严格配置提前暴露错误。这说明它可用于隔离的模型边界,但不能天然消除 ViewModel setter 的副作用。Mapster 官方项目Mapster 配置验证与编译

推荐写法

1. ViewModel 使用字段式 ObservableProperty

public sealed partial class OrderEditorViewModel : ObservableValidator
{
    [ObservableProperty]
    private string orderNumber = string.Empty;

    [ObservableProperty]
    private decimal amount;

    private readonly IOrderService orderService;

    private OrderEditorViewModel(
        OrderDetails model,
        IOrderService orderService)
    {
        this.orderService = orderService;

        // 初始化路径:直接写字段,不触发属性 setter 的通知和 hook。
        orderNumber = model.OrderNumber;
        amount = model.Amount;
    }

    public static OrderEditorViewModel Create(
        OrderDetails model,
        IOrderService orderService)
        => new(model, orderService);

    private UpdateOrderRequest CreateUpdateRequest()
        => new(OrderNumber, Amount);
}

CommunityToolkit.Mvvm 官方仍支持在字段上使用 [ObservableProperty],由生成器创建公共属性及通知逻辑。Microsoft:ObservableProperty 生成器

约束:

  • 构造期间写 backing field,表达“静默初始化”。
  • 用户交互和后续更新走生成属性,正常触发通知、验证与 Command 联动。
  • 不提供 Map<TSource, TDestination>() 式的全局通用接口。
  • Factory 按用途命名,例如 CreateForEditCreateSummaryRow,不要用含义模糊的 Map
  • ViewModel 依赖服务时,由 DI 注入 Factory 或由组合根调用构造逻辑;不要把服务塞入映射配置。

2. DTO 与模型之间使用普通扩展方法

internal static class OrderMappings
{
    public static OrderDetails ToDetails(this OrderResponse source)
        => new(
            source.Id,
            source.OrderNumber,
            source.Amount,
            source.UpdatedAt);

    public static UpdateOrderRequest ToUpdateRequest(
        this OrderEditorSnapshot source)
        => new(source.OrderNumber, source.Amount);
}

映射包含领域不变量时,不要用对象初始化器绕过领域方法:

public static Order ToDomain(this OrderResponse source)
    => Order.Restore(
        source.Id,
        source.OrderNumber,
        Money.FromDecimal(source.Amount),
        source.UpdatedAt);

3. 用工具生成“源码草稿”,不生成隐藏行为

可让 Visual Studio/GitHub Copilot 根据源类型、目标类型和以下规则生成普通方法:

为 OrderResponse -> OrderDetails 生成显式 C# 扩展方法。
逐项列出所有目标构造参数;不得使用反射、运行时映射器或动态代码;
字段名称不一致时不要猜测,保留编译错误或 TODO;同时生成关键字段测试。

生成结果必须:

  1. 成为正常 .cs 文件并提交 Git。
  2. 通过人工审查;金额、时间、枚举、null 和标识符转换重点检查。
  3. 随模型变更一起编译;构造函数参数变化会直接产生编译错误。
  4. 为关键映射补测试,不能仅断言目标对象非空。

Visual Studio 的 Copilot 能在解决方案上下文中编辑代码并执行验证,但生成内容仍应按普通代码审查。Microsoft:Visual Studio 中的 GitHub Copilot

什么时候才允许 Mapster

Mapster 不是默认依赖,仅当以下条件同时满足时才做小范围 POC:

  • 映射集中在 API、Infrastructure 或 Application 的纯数据类型之间。
  • 映射对数量和重复字段已经成为可度量的维护成本。
  • 目标对象没有通知、验证、生命周期 hook、服务依赖或领域不变量。
  • 团队愿意维护显式映射清单、严格配置和对应测试。

如果引入,规则是:

允许:Api.Contracts ↔ Application.Contracts
允许:只读查询投影的纯数据 DTO
禁止:任何 *.Presentation.* / *.ViewModels.* 目标类型
禁止:映射到长期存活且已经绑定 UI 的对象
禁止:用 AfterMapping 承载业务规则

并在启动测试或专门的配置测试中启用显式映射、目标成员检查并调用 Compile(),让错误尽早暴露。Mapster 的代码生成模式不作为本项目默认方案,以免重新引入生成器/工具链耦合。

新方案的代价

维度 显式映射方案
生产依赖 无新增对象映射包
源码行数 会增加,但可由 IDE/Copilot 起草
编译期反馈 强;类型或构造参数变化直接报错
调试 普通 C#,可直接单步
初始化副作用 可通过 backing field 静默初始化
业务语义 构造、恢复、编辑、提交路径清晰
维护成本 每次新增字段都需要明确决定是否映射
隐式遗漏风险 低于约定映射,但仍需要关键字段测试

最大的代价是多维护一些普通 C#;换来的收益是没有源码生成器顺序依赖、没有运行时映射配置,也不需要为了映射而改变 ViewModel 的属性设计。

落地规则

  1. 从新项目和新功能开始,不再添加 Riok.Mapperly
  2. 已有 Mapperly 映射不做无目的大爆炸迁移;修改到对应功能时,按边界逐个替换。
  3. 所有 ViewModel 映射优先改为构造函数或按用例命名的 Factory。
  4. DTO/模型映射放在靠近消费方的 Mappings 文件夹,保持依赖方向单向。
  5. .editorconfig、nullable、警告即错误和单元测试继续承担质量门禁。
  6. 如果以后评估 Mapster,单独写 ADR,并证明其范围不进入 Presentation 层。

对推荐技术栈闭环的修订

原方案:按需 Mapperly

修订为:
默认:显式构造函数 / Factory / 强类型扩展方法
增效:Visual Studio + GitHub Copilot 生成普通 C# 草稿
可选:Mapster 仅用于规模化、无副作用的纯数据模型边界
禁用:自动映射到 CommunityToolkit.Mvvm ViewModel