- 序列化
- 后端
【免费下载链接】MessagePack-CSharp
Extremely Fast MessagePack Serializer for C#(.NET, .NET Core, Unity, Xamarin). / msgpack.org[C#]
导读
本文深入解析 MessagePack-CSharp 源码生成器(Source Generator)内置分析器 MsgPack011 "Partial type required" 规则的触发原理、修复方法与背后设计动机。当你使用[MessagePackObject]标记的类型包含private/protected等非 internal 可见性的可序列化成员时,编译器会报出 MsgPack011 错误,要求类型声明为partial。读完本文,你将理解该规则为何存在、在什么条件下触发、如何通过自动代码修复或手工调整可见性消除错误,以及嵌套类型场景下的特殊约束,并能够结合源码与测试用例独立排查同类问题。
一、MsgPack011 规则概述
1.1 规则身份信息
MsgPack011 是随 MessagePackAnalyzer NuGet 包一同分发的一组 Roslyn 诊断分析器之一(完整清单见 doc/analyzers/index.md)。在源码中,该规则的 ID 常量定义于 src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs:
public const string PartialTypeRequiredId = "MsgPack011";其诊断描述符(DiagnosticDescriptor)的关键信息如下(见 MsgPack00xMessagePackAnalyzer.cs):
| 属性 | 值 |
|---|---|
| ID | MsgPack011 |
| 标题(Title) | Partial type required |
| 消息(Message) | Types with private, serializable members must be declared as partial, including nesting types |
| 严重级别 | Error(错误) |
| 默认启用 | 是(isEnabledByDefault: true) |
该规则定位为编译错误(Error),而非警告。这意味着一旦触发,你的项目将无法正常编译通过,必须处理。
1.2 规则的官方说明
按 doc/analyzers/MsgPack011.md 的原始描述:
当一个
[MessagePackObject]包含可见性低于internal的可序列化成员时,声明该成员的类型必须使用partial修饰符,以便源码生成的 formatter 可以作为该类型的一个成员(嵌套类型)被发出,从而获得对私有成员的访问权。
换言之,MsgPack011 不是一条随意的代码风格约束,而是源码生成器能否正确工作的一道硬性前置条件。
二、触发原理:为什么私有成员需要 partial?
2.1 源码生成器的基本工作方式
MessagePack-CSharp 的源码生成器(src/MessagePack.SourceGenerator/MessagePackGenerator.cs 中的MessagePackGenerator,实现IIncrementalGenerator)会在编译期间为带[MessagePackObject]的类型自动生成对应的IMessagePackFormatter<T>实现(如XxxFormatter),包括序列化(Serialize)与反序列化(Deserialize)逻辑。
关键设计点是:生成的 formatter 代码会被作为目标类型内部的嵌套类型发出(nested type),而不是一个完全独立的顶级类。这样做的直接好处是:嵌套类型天然拥有外层类型的全部访问权,包括private成员——这正是访问私有可序列化字段/属性所必需的。
然而,C# 语言规定:只有声明为partial的类型,才能由多个源代码片段共同组成。生成器要把 formatter 作为该类型的另一部分成员注入进去,就必须保证该类型声明了partial修饰符,否则生成器根本没有合法的语法位置来"补写"这段代码。
2.2 触发条件的判定逻辑
在 src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs 中,收集器首先判定是否需要嵌套 formatter:
// If any property had a private setter and does not appear in the deserializing constructor signature, // we'll need a nested formatter. foreach (IPropertySymbol property in nestedFormatterRequiredIfPropertyIsNotSetByDeserializingCtor) { nestedFormatterRequired |= !constructorParameters.Any(m => m.Name == property.Name); } if (nestedFormatterRequired && nonPublicMembersAreSerialized) { // If the data type or any nesting types are not declared with partial, we cannot emit the formatter as a nested type within the data type // as required in order to access the private members. bool anyNonPartialTypesFound = false; BaseTypeDeclarationSyntax[] nonPartialTypes = FindNonPartialTypes(formattedType).ToArray(); if (nonPartialTypes.Length > 0) { ... this.reportDiagnostic?.Invoke(Diagnostic.Create(MsgPack00xMessagePackAnalyzer.PartialTypeRequired, primaryLocation, (IEnumerable<Location>?)addlLocations)); anyNonPartialTypesFound = true; } if (anyNonPartialTypesFound) { return null; // 放弃收集该类型,避免生成错误的 formatter } }两个条件缺一不可:
nestedFormatterRequired为真:类型中存在需要嵌套 formatter 才能访问的成员。这通常源于成员可见性过低,或者私有 setter 未出现在反序列化构造函数签名中;nonPublicMembersAreSerialized为真:确实有非 public 成员被[Key(...)]标记参与序列化。
2.3 哪些可见性会触发"必须 partial"?
判定函数位于 TypeCollector.cs:
private static bool IsPartialTypeRequired(Accessibility accessibility) => accessibility is not (Accessibility.Public or Accessibility.Internal or Accessibility.ProtectedOrInternal);从源码结构可以提炼出如下可见性对照表:
| 成员可见性 | 是否需要 partial? | 原因 |
|---|---|---|
public | 否 | 任何外部代码均可访问 |
internal | 否 | 同程序集的 formatter 可访问 |
protected internal | 否 | 兼具 protected 与 internal 访问权 |
private | 是 | 仅类型自身可访问,必须靠嵌套 formatter |
protected | 是 | 仅类型及派生类可访问,生成器从外部无法触达 |
private protected | 是 | 仅类型及程序集内派生类可访问 |
| 无修饰符(class 内默认 private) | 是 | 同 private |
这一判定被应用于字段、属性的 getter/setter 以及构造函数等多个位置(TypeCollector.cs、TypeCollector.cs、TypeCollector.cs),说明该规则覆盖所有形式的可序列化成员与反序列化构造函数。
2.4 嵌套类型:外层类型也要 partial
一个容易遗漏的细节:如果[MessagePackObject]类型本身是嵌套类型(定义在另一个类内部),那么从目标类型到最外层声明类型的整条链上,每一层都必须声明partial。
实现上通过FindNonPartialTypes遍历"类型及其所有包含类型"(TypeCollector.cs):
private static IEnumerable<BaseTypeDeclarationSyntax> FindNonPartialTypes(ITypeSymbol target) { return from x in CodeAnalysisUtilities.EnumerateTypeAndContainingTypes(target) where x.FirstDeclaration?.Modifiers.Any(SyntaxKind.PartialKeyword) is false select x.FirstDeclaration; }EnumerateTypeAndContainingTypes会沿ContainingType链向上递归,任何一层缺少partial都会被标记为 non-partial,并作为附加位置(additional locations)一并报告。这意味着错误信息会同时高亮"内层类型缺 partial"和"外层包含类型缺 partial"两处代码位置。
三、被标记的典型模式
以下代码会触发 MsgPack011(示例来自 doc/analyzers/MsgPack011.md):
[MessagePackObject] public class A { [Key(0)] private B b; }这里b是private可见性的可序列化成员。生成器若要为A生成 formatter 并读写A.b,只能把 formatter 作为A的嵌套类型发出;而A没有partial修饰符,语法上无法容纳生成代码,于是编译器报出 MsgPack011 错误。
再举一个属性场景:私有 setter 且未被反序列化构造函数覆盖的属性同样触发:
[MessagePackObject] public class Order { [Key(0)] public int Id { get; private set; } }只要生成器需要直接为private set赋值,nestedFormatterRequired即为真;结合nonPublicMembersAreSerialized,同样要求Order为partial。
四、修复方式一:添加 partial 修饰符(推荐)
4.1 手工修复
按 doc/analyzers/MsgPack011.md 给出的典型修复,为类型添加partial关键字即可:
[MessagePackObject] public partial class A { [Key(0)] private B b; }添加partial后,生成器就可以合法地在该类型的另一部分(partial 声明片段)中注入嵌套 formatter 类,从而访问A的私有成员。
4.2 自动代码修复(Code Fix)
该规则配套了自动代码修复。在 src/MessagePack.Analyzers.CodeFixes/CodeFixes/FormatterCodeFixProvider.cs 中注册了名为"Add partial modifier"的 Code Action:
case MsgPack00xMessagePackAnalyzer.PartialTypeRequiredId when typeDecl is not null: context.RegisterCodeFix( CodeAction.Create( "Add partial modifier", ct => AddPartialModifierAsync(context.Document, syntaxRoot, typeDecl, diagnostic, ct), "AddPartialModifier"), diagnostic); break;实际改写逻辑(FormatterCodeFixProvider.cs)会在类型声明上追加partial修饰符:
private static bool TryAddPartialModifier(BaseTypeDeclarationSyntax typeDeclaration, [NotNullWhen(true)] out BaseTypeDeclarationSyntax? modified) { if (typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) { modified = null; return false; // 已存在 partial,无需修改 } modified = typeDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); return true; }在 IDE(Visual Studio / VS Code + C# Dev Kit 等)中,将光标置于 MsgPack011 错误上,按 Ctrl+.(Quick Actions),选择 "Add partial modifier",即可一键修复。对于嵌套类型场景,Code Fix 会为目标类型逐层补上partial。
该 Code Fix 有对应的测试验证(tests/MessagePack.SourceGenerator.Tests/MsgPack011PartialTypeRequiredTests.cs):测试源码中public class MyObject被标记为{|MsgPack011:MyObject|},修复后的期望代码为public partial class MyObject,验证了 Code Fix 从"报错源码"到"修复源码"的完整转换。
五、修复方式二:将成员提升为 internal(替代方案)
如果不希望类型变成partial,可以反其道而行:把私有成员提升为internal可见性。internal成员对整个程序集可见,因此生成器可以把 formatter 作为程序集内的独立类型发出,而无需嵌套进目标类型内部。
按 doc/analyzers/MsgPack011.md 的替代方案示例:
[MessagePackObject] public class A { [Key(0)] internal B b; }从上一节的可见性对照表可以看出,internal、protected internal、public三种可见性都不会触发 MsgPack011,因为它们足以让位于程序集任意位置的生成 formatter 直接访问。
六、嵌套类型场景:整条类型链都要 partial
6.1 场景复现
当[MessagePackObject]类型是另一个类的嵌套类型时,MsgPack011 会要求自身及所有外层包含类型都声明partial。这与 MsgPack00xMessagePackAnalyzer.cs 中消息文本"including nesting types"一致。
测试 MsgPack011PartialTypeRequiredTests.cs 给出了完整示例:
[MessagePackObject] public class Outer { [MessagePackObject] internal class Inner { [Key(0)] private Outer Value { get; set; } } }Inner的私有成员Value需要嵌套 formatter,因此Inner必须为partial;又因为 formatter 要作为Inner的嵌套类型发出,而Inner嵌套在Outer中,Outer也必须为partial,否则类型链断裂。修复后的正确形态:
[MessagePackObject] public partial class Outer { [MessagePackObject] internal partial class Inner { [Key(0)] private Outer Value { get; set; } } }6.2 诊断的多位置报告
该场景的诊断会在主位置(primary location)高亮目标类型Inner的标识符,同时把缺少partial的外层类型Outer的位置作为附加位置(additional locations)一并报告(TypeCollector.cs)。因此修复时请留意 IDE 中同一错误下的多处高亮,逐层补齐partial。
七、触发后的行为:生成器放弃该类型
值得强调的一个细节:当检测到非 partial 类型时,TypeCollector 不仅报告 MsgPack011,还会直接返回 null、放弃为当前类型收集序列化信息(TypeCollector.cs)。这意味着:
- 该类型的 formatter不会被生成;
- 依赖它的其他类型(如引用该类型的父对象)会随之产生级联的编译错误或运行时缺失 formatter 的异常(如
MessagePackSerializationException/ FormatterNotRegistered)。
所以 MsgPack011 虽然定位为 Error,但它的意义远不止"警告你加个 partial"——它实际上在保护生成器不产出无法访问私有成员的残缺 formatter 代码。
八、与相邻规则的区分
MessagePack 分析器家族中还有几个容易混淆的相关规则,建议对照 doc/analyzers/index.md 一并理解:
| 规则 ID | 标题 | 关注点 | 与 MsgPack011 的区别 |
|---|---|---|---|
| MsgPack011 | Partial type required | 类型是否声明partial | 本规则:语法层面能否容纳嵌套 formatter |
| MsgPack012 | Inaccessible data type | 类型可见性是否至少internal | 关注类型本身的可见性,而非成员可见性 |
| MsgPack013 | Inaccessible formatter instance | formatter 是否可被解析器构造 | 关注 formatter 的构造器/单例字段 |
| MsgPack015 | MessagePackObjectAttribute.AllowPrivate should be set | 非 public 类型/成员需要显式开启AllowPrivate | 关注属性开关,而非 partial 语法 |
实际项目中,一个包含私有成员的类型往往同时触发 MsgPack011 与 MsgPack015(后者要求设置AllowPrivate = true)。例如 PrivateMemberAccessTests.cs 中的测试同时标记了{|MsgPack011:MyObject|}。处理顺序建议是:先按 MsgPack015 在[MessagePackObject]上开启AllowPrivate = true(如[MessagePackObject(AllowPrivate = true)]),再处理 MsgPack011 的partial要求;也可以直接使用 Code Fix 逐个修复。
九、实战检查清单
在你的项目中遇到 MsgPack011 时,按以下顺序排查:
- 确认触发成员:找到错误高亮类型中被
[Key(...)]标记、可见性为private/protected/private protected或私有 setter 的字段/属性; - 选择修复路线:
- 想保留封装性 → 给类型加
partial(IDE 中直接应用 "Add partial modifier" Code Fix); - 类型形态不便改动(如匿名内部类、部分框架类型)→ 将成员可见性提升为
internal;
- 想保留封装性 → 给类型加
- 检查嵌套链:若类型嵌套在别的类中,确认从自身到最外层每一层都声明了
partial; - 联动检查 MsgPack015:非 public 类型或含非 public 成员时,确认
[MessagePackObject(AllowPrivate = true)]已设置; - 重新编译:由于生成器在 MsgPack011 触发时会放弃该类型的 formatter 收集,修复后务必全量重新构建,确认没有级联的"缺少 formatter"类错误残留。
十、源码与测试索引
便于深入研究的仓库关键位置:
- 规则文档:doc/analyzers/MsgPack011.md
- 规则 ID 与描述符定义:src/MessagePack.SourceGenerator/Analyzers/MsgPack00xMessagePackAnalyzer.cs
- 触发判定与诊断上报:src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
- 可见性判定函数:src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
- 非 partial 类型链扫描:src/MessagePack.SourceGenerator/CodeAnalysis/TypeCollector.cs
- 自动修复 "Add partial modifier":src/MessagePack.Analyzers.CodeFixes/CodeFixes/FormatterCodeFixProvider.cs
- 单元测试(含嵌套类型场景):tests/MessagePack.SourceGenerator.Tests/MsgPack011PartialTypeRequiredTests.cs
- 相邻规则索引:doc/analyzers/index.md
结语
MsgPack011 是 MessagePack-CSharp 源码生成器保证"能生成、生成对"的一道关键防线:它把 C# 语言的partial机制与源码生成的嵌套 formatter 设计紧密结合。理解它的触发条件(私有/受保护可序列化成员)、两种修复路线(加partial或提升为internal)以及嵌套类型的链路要求,能让你在 AOT 场景下少走弯路,也让自动生成的 formatter 顺畅访问那些被封装保护的成员。
- 序列化
- 后端
【免费下载链接】MessagePack-CSharp
Extremely Fast MessagePack Serializer for C#(.NET, .NET Core, Unity, Xamarin). / msgpack.org[C#]
相关推荐
MessagePack-CSharp 源码生成器诊断 MsgPack003:为被引用类型补齐 `[MessagePackObject]` 标注
MessagePack CSharp 源码生成器诊断 MsgPack003:为被引用类型补齐 MessagePackObject 标注 本篇文章讲解 Messa
序列化后端MessagePack-CSharp MsgPack012 诊断:可序列化数据类型必须至少为 internal 可访问性
MessagePack CSharp MsgPack012 诊断:可序列化数据类型必须至少为 internal 可访问性 导读 MsgPack012(Inacc
序列化后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考