Mapperly 与 CommunityToolkit.Mvvm 源码生成器兼容性¶
- 归档日期:2026-08-25
- 适用基线:
.NET 10 LTS + C# 14 + WPF + CommunityToolkit.Mvvm - 讨论问题:Roslyn 不能配置源码生成器执行顺序,Mapperly 是否因此无法与 CommunityToolkit.Mvvm 一起使用?
后续决策(2026-08-25):项目不采用 partial property + Mapperly 作为 ViewModel 映射方案,改用显式构造函数、Factory 和强类型扩展方法。本文保留为兼容性分析记录;现行方案见 WPF 项目用显式映射替代 Mapperly。
结论¶
这个说法“一半正确,一半过度概括”。
正确部分:
- Roslyn 的传统源码生成器没有可由应用项目配置的先后顺序。
- 每个生成器默认看到同一份初始 Compilation,看不到其他生成器在本轮输出的普通生成源码。
- 因此,Mapperly 不能依赖 CommunityToolkit.Mvvm 先生成一个原始代码中完全不存在的成员。
- 使用传统字段写法
[ObservableProperty] private string name;时,Mapperly 在同一项目中确实看不到最终生成的公共Name属性。
不准确部分:
- 这不代表 Mapperly 与 CommunityToolkit.Mvvm 不能安装在同一个项目,也不代表两者所有功能都不兼容。
- 使用 C# 13 引入、在 .NET 10/C# 14 中稳定可用的
partial property写法时,公共属性的“定义声明”已经存在于原始源码中;Mapperly 可以看到其签名,MVVM Toolkit 只负责生成实现。 - 这种写法不需要两个生成器互相读取输出,因此不依赖生成器排序。
在 2026-08-25 的推荐判断是:
Mapperly 映射普通 DTO / Domain / Application Model
→ 完全可以与 CommunityToolkit.Mvvm 共存
Mapperly 映射采用 partial property 的纯数据 ViewModel
→ 当前稳定组合可以工作,但要控制通知副作用
Mapperly 映射采用字段式 [ObservableProperty] 生成属性的 ViewModel
→ 同项目内不可依赖该生成属性,不推荐
一、Roslyn 的限制确实存在¶
Roslyn 官方源码生成器设计明确说明:生成器以无序方式运行,每个生成器看到相同的输入 Compilation,不能访问其他生成器创建的普通生成文件。Roslyn:Source Generators
Mapperly 自己的 FAQ 也明确写明:如果它依赖另一个生成器的输出,Roslyn 不支持这种 source generator chaining。Mapperly FAQ
所以,下面这条推理是正确的:
原始源码只有 private 字段 name
→ MVVM Toolkit 将来才生成 public Name
→ Mapperly 本轮只能看到 private name,看不到 public Name
→ 自动映射无法发现 Name
这不是调整 .csproj 中 PackageReference 顺序能够解决的问题,也不能通过 MSBuild 的 BeforeTargets / AfterTargets 给同一次 Roslyn 生成阶段排序。
截至 2026-08-25,Roslyn 已出现实验性的 pre-compilation source output API,用于让特定早期输出对后续生成阶段可见,但这不是通用的“用户配置生成器顺序”,而且要求生成器作者采用新的实验管线。现有应用不应把它当作 Mapperly 与 MVVM Toolkit 的生产解决方案。Roslyn:Pre-compilation Generator API
二、传统字段式 ObservableProperty 为什么失败¶
public sealed partial class PersonViewModel : ObservableObject
{
[ObservableProperty]
private string name = string.Empty;
}
MVVM Toolkit 最终会生成:
但 Name 完全来自 MVVM Toolkit 的输出。Mapperly 执行时看不到它。
官方 Mapperly 讨论中,同样的 CommunityToolkit.Mvvm 场景会出现“找不到生成属性”的问题,维护者给出的原因正是 .NET 不支持 generator chaining。Mapperly Discussion #1445
本次使用以下稳定组合进行了最小编译验证:
双向映射 PersonModel ↔ PersonViewModel 时,字段写法得到:
RMG020: PersonModel.Name 没有映射到 PersonViewModel 成员
RMG012: PersonModel.Name 在 PersonViewModel 源类型中找不到
RMG066: 对象映射没有映射任何成员
如果项目没有把这些 Mapperly 警告提升为错误,构建甚至可能成功,但生成的映射只会 new PersonViewModel() 而不复制数据。这比直接编译失败更危险。
三、partial property 为什么可以工作¶
C# 从 13 开始支持 partial property。一个源码文件提供定义声明,另一个部分——包括源码生成器——提供实现。只有定义声明参与正常成员查找。Microsoft:Partial Properties
使用当前写法:
public sealed partial class PersonViewModel : ObservableObject
{
[ObservableProperty]
public partial string Name { get; set; } = string.Empty;
}
这时原始 Compilation 中已经有:
两个生成器分别完成不同工作:
Mapperly
读取原始源码中的 PersonViewModel.Name 定义声明
生成 target.Name = source.Name
CommunityToolkit.Mvvm
读取 [ObservableProperty] partial property
生成 Name 的 getter/setter 实现和通知逻辑
C# 编译器
合并 partial property 定义与实现
编译 Mapperly 对 Name 的正常访问
两者都只依赖用户写出的 Name 定义声明,并不需要读取对方的生成结果,所以执行顺序不重要。
使用上述相同版本的实际验证结果:
即 Model → ViewModel → Model 双向映射成功。
CommunityToolkit.Mvvm 8.4.1 将生成器升级到 Roslyn 5.0,使 partial property 在 C# 14 中不再需要 LangVersion=preview;当前稳定版 8.4.2 包含该支持。CommunityToolkit 8.4.1 Release;CommunityToolkit.Mvvm NuGet
四、可工作的最小示例¶
using CommunityToolkit.Mvvm.ComponentModel;
using Riok.Mapperly.Abstractions;
public sealed class PersonModel
{
public string Name { get; set; } = string.Empty;
}
public sealed partial class PersonViewModel : ObservableObject
{
[ObservableProperty]
public partial string Name { get; set; } = string.Empty;
}
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Both)]
public static partial class PersonMapper
{
public static partial PersonViewModel ToViewModel(PersonModel source);
public static partial PersonModel ToModel(PersonViewModel source);
}
建议同时将关键 Mapperly 诊断提升为错误:
# .editorconfig
[*.cs]
dotnet_diagnostic.RMG012.severity = error
dotnet_diagnostic.RMG020.severity = error
dotnet_diagnostic.RMG066.severity = error
Mapperly 4 默认已经采用严格映射并发出 warning,但正式项目应避免警告被忽略。Mapperly:RequiredMappingStrategy;Mapperly:RMG066
五、RelayCommand 等其他生成成员¶
[RelayCommand] 生成的 SaveCommand、LoadCommand 等成员同样不对其他普通生成器可见。但 Mapperly 本来就不应映射命令、Messenger、CancellationToken、Service 或 UI 状态机。
因此推荐:
如果某个设计要求 Mapperly 发现并映射由 [RelayCommand] 等生成器完全创建的成员,则仍会遇到 Roslyn chaining 限制,而且这通常说明映射边界设计错了。
六、架构上的最佳做法¶
方案 A:Mapperly 不直接映射完整 ViewModel——最推荐¶
API DTO
↕ Mapperly
Application Model / Domain DTO
↓ 手写 ViewModel factory / constructor
WPF ViewModel
示例:
public sealed partial class PersonViewModel : ObservableObject
{
public PersonViewModel(PersonModel model, IPersonService service)
{
id = model.Id;
Name = model.Name;
this.service = service;
}
private readonly int id;
private readonly IPersonService service;
[ObservableProperty]
public partial string Name { get; set; }
}
原因:真正的 ViewModel 往往还包含:
- Command 和 CanExecute。
- 服务依赖和 CancellationToken。
- Loading / Empty / Error / Validation 状态。
- 选择、导航、对话框和生命周期。
- 属性变化 hook 和联动通知。
把它当 DTO 自动映射,会把重要初始化语义隐藏在属性赋值中。Mapperly 更适合边界模型之间的机械映射。
方案 B:映射“纯数据行 ViewModel”——可以使用¶
例如 DataGrid 行模型只包含可编辑数据,没有服务、命令和生命周期,可以使用 partial property:
public sealed partial class OrderRowViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
public partial string OrderNumber { get; set; } = string.Empty;
[ObservableProperty]
public partial decimal Amount { get; set; }
}
仍需注意 Mapperly 通过 setter 赋值会触发属性通知、验证和 OnXxxChanged hook。创建新实例时通常没有订阅者,但 hook 仍可能执行;更新长期存活的现有 ViewModel 时更要谨慎。
方案 C:字段式 ObservableProperty + 手写映射——简单可靠¶
如果团队更喜欢字段写法,就不要让 Mapperly映射这些 ViewModel:
手写普通 C# 代码是在所有生成器完成后一起编译的,因此可以正常引用 MVVM Toolkit 生成的成员;这里受限的是“生成器读取另一个生成器的输出”,不是普通业务代码使用生成成员。
方案 D:拆为两个程序集——可行但通常不划算¶
Presentation.Models
编译 CommunityToolkit.Mvvm 生成的 ViewModel
Presentation.Mapping
引用 Presentation.Models.dll
Mapperly 从程序集元数据读取已生成的公共属性
这种方式能打破同一次 Compilation 的限制,但会新增项目、依赖方向和部署复杂度。只有多个应用确实共享一套纯数据 ViewModel/映射层时才值得采用。
七、不要采用的“修复方式”¶
- 调整两个 NuGet PackageReference 的先后顺序。
- 尝试给 Analyzer/Generator 配置 MSBuild
BeforeTargets。 - 依赖一次失败构建、第二次成功的偶然行为。
- 把生成文件复制到源码目录并与生成器同时启用。
- 关闭
RMG012/RMG020/RMG066来让空映射通过。 - 为了自动映射而把服务、命令或业务状态暴露成可写属性。
这些做法要么无法改变 Roslyn 同一次生成阶段的 Compilation,要么会产生重复符号、脆弱构建或静默丢数据。
八、对推荐技术栈的修正¶
原“开发效率增强技术栈”中对 Mapperly 的推荐应收紧为:
默认不安装 Mapperly
出现大量机械 DTO 映射时:
Mapperly 用于 API / Infrastructure / Application / Domain 边界
确实需要映射 ViewModel 时:
仅映射纯数据 ViewModel
优先使用 [ObservableProperty] public partial Property
CommunityToolkit.Mvvm >= 8.4.1
开启 RMG012 / RMG020 / RMG066 errors
验证通知、验证和 hook 副作用
完整 ViewModel:
使用显式构造函数或 factory
因此,Mapperly 仍可作为按需增效工具保留,但不应被描述成可以无条件映射所有 CommunityToolkit.Mvvm ViewModel。
关于 partial property 方案在初始化通知、验证、Command 联动和维护方面的具体成本,见 Mapperly 与 MVVM Toolkit Partial Property 方案的代价。