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 按用途命名,例如
CreateForEdit、CreateSummaryRow,不要用含义模糊的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;同时生成关键字段测试。
生成结果必须:
- 成为正常
.cs文件并提交 Git。 - 通过人工审查;金额、时间、枚举、null 和标识符转换重点检查。
- 随模型变更一起编译;构造函数参数变化会直接产生编译错误。
- 为关键映射补测试,不能仅断言目标对象非空。
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 的属性设计。
落地规则¶
- 从新项目和新功能开始,不再添加
Riok.Mapperly。 - 已有 Mapperly 映射不做无目的大爆炸迁移;修改到对应功能时,按边界逐个替换。
- 所有 ViewModel 映射优先改为构造函数或按用例命名的 Factory。
- DTO/模型映射放在靠近消费方的
Mappings文件夹,保持依赖方向单向。 .editorconfig、nullable、警告即错误和单元测试继续承担质量门禁。- 如果以后评估 Mapster,单独写 ADR,并证明其范围不进入 Presentation 层。