跳转至

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 最终会生成:

public string Name
{
    get => name;
    set => SetProperty(ref name, value);
}

Name 完全来自 MVVM Toolkit 的输出。Mapperly 执行时看不到它。

官方 Mapperly 讨论中,同样的 CommunityToolkit.Mvvm 场景会出现“找不到生成属性”的问题,维护者给出的原因正是 .NET 不支持 generator chaining。Mapperly Discussion #1445

本次使用以下稳定组合进行了最小编译验证:

.NET SDK                  10.0.102,allowPrerelease=false
CommunityToolkit.Mvvm     8.4.2
Riok.Mapperly             4.3.1

双向映射 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 中已经有:

public string Name { get; set; } 的定义声明

两个生成器分别完成不同工作:

Mapperly
  读取原始源码中的 PersonViewModel.Name 定义声明
  生成 target.Name = source.Name

CommunityToolkit.Mvvm
  读取 [ObservableProperty] partial property
  生成 Name 的 getter/setter 实现和通知逻辑

C# 编译器
  合并 partial property 定义与实现
  编译 Mapperly 对 Name 的正常访问

两者都只依赖用户写出的 Name 定义声明,并不需要读取对方的生成结果,所以执行顺序不重要。

使用上述相同版本的实际验证结果:

dotnet run -c Release
Ada|Ada

Model → ViewModel → Model 双向映射成功。

CommunityToolkit.Mvvm 8.4.1 将生成器升级到 Roslyn 5.0,使 partial property 在 C# 14 中不再需要 LangVersion=preview;当前稳定版 8.4.2 包含该支持。CommunityToolkit 8.4.1 ReleaseCommunityToolkit.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:RequiredMappingStrategyMapperly:RMG066

五、RelayCommand 等其他生成成员

[RelayCommand] 生成的 SaveCommandLoadCommand 等成员同样不对其他普通生成器可见。但 Mapperly 本来就不应映射命令、Messenger、CancellationToken、Service 或 UI 状态机。

因此推荐:

映射对象只包含数据成员
命令和服务不参与映射
Mapperly 对命令成员既不读取也不写入

如果某个设计要求 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:

public static PersonViewModel ToViewModel(PersonModel source)
    => new(source.Id, source.Name);

手写普通 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 方案的代价