关于 dotnet-aot-compat
用于处理分析器警告、使目标为 .NET 8 或更高版本的项目兼容 Native AOT 和裁剪的技能。它以构建结果驱动排查,优先保留注解传递或重构,而非抑制警告。
主要功能
- 01启用 AOT 与裁剪分析
- 02按警告代码分类问题
- 03添加反射注解或重构调用
- 04使用源生成 JSON 序列化
- 05验证多个目标框架构建
适用场景
- 01修复 IL 裁剪与 AOT 警告
- 02为反射代码补充注解
- 03让 .NET 项目兼容 Native AOT
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill dotnet-aot-compat安装后在 Claude Code 中输入/dotnet-aot-compat确认已加载。
附带文件references/polyfills.md
详细介绍
dotnet-aot-compat
通过系统地解决所有 IL 修剪器/AOT 分析器警告,使 .NET 项目兼容 Native AOT 和修剪。
适用场景
- “使此项目兼容 AOT”
- “修复修剪警告”或“修复 IL 警告”
- “解决 IL2070 / IL2067 / IL2072 / IL2026 / IL3050 警告”
- “添加 DynamicallyAccessedMembers 注解”
- “在我的 .csproj 中启用 IsAotCompatible”
- “升级到 net8.0 后,我的项目出现修剪分析器警告”
- “为修剪器标注反射代码”
不适用场景
如果项目仅面向 .NET Framework(net4x),请勿使用此技能,因为它不支持修剪/AOT 分析器。
前提条件
现有的 .NET 项目,目标为 net8.0 或更高版本(或多目标项目中至少包含一个 net8.0+ TFM),并已安装相应的 .NET SDK。
背景:AOT 兼容性意味着什么
Native AOT 和 IL 修剪器会执行静态分析,以确定哪些代码可达。反射可能会破坏此分析,因为修剪器无法获知运行时访问了哪些类型/成员。IsAotCompatible 属性会启用分析器,将这些问题标记为构建警告(ILXXXX 代码)。
重要规则
❌ 切勿错误地抑制警告
- 切勿对 IL 警告使用
#pragma warning disable。这会在构建时向 Roslyn 分析器隐藏警告,但 IL 链接器和 AOT 编译器仍然会发现问题。代码会在修剪/发布时失败。 - 切勿使用
[UnconditionalSuppressMessage]。它会让分析器和链接器都忽略警告,意味着修剪器无法验证安全性。始终应优先在构建时引发错误,而不是隐藏问题,导致其在运行时悄然出错。
💡 首选方法
- 优先使用
[DynamicallyAccessedMembers]注解,在调用链中传递类型信息。 - 优先重构代码,消除会破坏注解流转的模式(例如,通过
object[]装箱Type)。 - 使用
[RequiresUnreferencedCode]/[RequiresDynamicCode]/[RequiresAssemblyFiles]标记从根本上与修剪不兼容的方法,并将此要求传递给调用方。这样可以明确暴露问题,而不是隐藏问题——调用方必须明确承认这种不兼容性。
注解流转是关键
修剪器会跟踪 [DynamicallyAccessedMembers] 注解在赋值、参数传递和返回值中的流转。如果此流转被破坏(例如,将 Type 装箱为 object、存储在非类型化集合中,或通过接口进行转换),修剪器就会丢失跟踪信息并发出警告。正确的修复方式是保留流转,而不是抑制警告。
分步操作流程
不要预先探索代码库。 构建警告会准确指出需要修改的文件和行号。请遵循紧凑的循环:构建 → 选择一条警告 → 打开该文件对应行 → 按照修复方法处理 → 重新构建。阅读或分析特定警告未指向的源文件是浪费精力,还可能导致超时。让编译器引导你。
❌ 构建之前,不要运行
find、ls或grep来了解项目结构。不要阅读 README、文档或架构文件。第一步应是步骤 1(启用 AOT 分析),然后构建。
步骤 1:在 .csproj 中启用 AOT 分析
添加 IsAotCompatible。如果项目并非仅面向 net8.0+,请添加 TFM 条件(AOT 分析要求 net8.0+):
<PropertyGroup>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
</PropertyGroup>
此设置会自动为兼容的 TFM 设置 EnableTrimAnalyzer=true 和 EnableAotAnalyzer=true。对于多目标项目(例如 netstandard2.0;net8.0),该条件可确保旧版 TFM 不会出现 NETSDK1210 警告。
步骤 2:构建并收集警告
dotnet build <project.csproj> -f <net8.0-or-later-tfm> --no-incremental 2>&1 | grep 'IL[0-9]\{4\}'
对警告进行排序并去重。常见警告代码:
- IL2070:对缺少
[DynamicallyAccessedMembers]的Type参数调用反射 - IL2067:将未注释的
Type传递给需要[DynamicallyAccessedMembers]的方法 - IL2072:返回值或提取的值缺少注释(通常由拆箱导致)
- IL2057:
Type.GetType(string)使用了非恒定参数 - IL2026:调用标记了
[RequiresUnreferencedCode]的方法 - IL2050:带有 COM 封送参数的 P/invoke 方法
- IL2075:返回值流入反射操作,但缺少注释
- IL2091:泛型参数缺少约束要求的
[DynamicallyAccessedMembers] - IL3000:在单文件/AOT 应用中,
Assembly.Location返回空字符串 - IL3050:调用标记了
[RequiresDynamicCode]的方法
步骤 3:按代码对警告分类(不要逐个读取文件)
按警告代码对步骤 2 中的警告分组并计数。**暂时不要打开单个文件。**找出数量最多的 1–2 种模式,它们将决定你的修复策略:
| 模式 | 常见修复方法 |
|---|---|
来自 JsonSerializer 的大量 IL2026 + IL3050 | 立即采用策略 C——创建 JsonSerializerContext,然后批量更新所有调用点 |
Type 参数上的 IL2070/IL2087 | 为最内层方法添加 [DynamicallyAccessedMembers],然后逐层向外添加 |
传递未注释的 Type 时出现 IL2067 | 在源头为参数添加注释 |
**在大多数实际项目中,来自 JsonSerializer 的 IL2026/IL3050 占主导地位。**除非警告统计明显显示其他情况,否则请从策略 C 开始。批量修复 JSON 后,再使用策略 A–B 处理剩余警告。策略 D 仅作为最后手段。
步骤 4:迭代修复警告(从最内层开始)
从最内层的反射调用开始,逐步向外处理。每次修复都可能使新的警告传播到调用方。
**以警告为导向。**对于每条警告,只打开编译器报告的文件和行,识别其模式,应用下方对应的修复方法,然后继续。不要扫描代码库来寻找类似模式,也不要试图理解整体架构——只修复编译器指出的问题,重新构建,并根据新出现的警告决定下一步。每次修复一小批警告(5–10 条),然后立即重新构建以检查进展。
在可用时使用子 Agent。如果可以启动子 Agent(例如通过 task 工具),请并行派遣多个子 Agent,同时编辑不同文件。让主循环专注于构建、解析警告和派遣任务——将实际文件编辑委派给子 Agent。对于批量 JSON 更新,在一条提示词中分配给每个子 Agent 5–10 个文件。**经过 2 轮构建和修复后,并行派遣子 Agent 编辑所有剩余文件——不要继续逐个修复文件。**示例:
更新这些文件以使用源生成的 JSON:
src/Models/Resource.Serialization.cs、src/Models/Identity.Serialization.cs、src/Models/Plan.Serialization.cs。在每个文件中,将JsonSerializer.Serialize(writer, value)替换为JsonSerializer.Serialize(writer, value, MyProjectJsonContext.Default.TypeName),并将JsonSerializer.Deserialize<T>(ref reader)替换为JsonSerializer.Deserialize(ref reader, MyProjectJsonContext.Default.TypeName)。只编辑 JsonSerializer 调用点。
策略 A:添加 [DynamicallyAccessedMembers](首选)
当方法对 Type 参数使用反射时,为参数添加注释,告知修剪器需要保留哪些成员:
using System.Diagnostics.CodeAnalysis;
// Before (warns IL2070):
void Process(Type t) {
var method = t.GetMethod("Foo"); // trimmer can't verify
}
// After (clean):
void Process([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) {
var method = t.GetMethod("Foo"); // trimmer preserves public methods
}
为参数添加注释后,所有调用方都必须传递带有适当注释的类型。这会逐层向外传播——跟进每个调用方,并根据需要添加注释或重构。**调用方的注释必须至少包含与被调用方相同的成员类型。**如果被调用方需要 PublicConstructors | NonPublicConstructors,调用方必须指定相同的内容或其超集——只使用 NonPublicConstructors 会产生 IL2091。
策略 B:重构以保留注解流
当装箱(将 Type 存储在 object、object[] 或无类型集合中)导致注解流中断时,重构代码,直接传递 Type:
// BROKEN: Type boxed into object[], annotation lost
void Process(object[] args) {
Type t = (Type)args[0]; // IL2072: annotation lost through boxing
Evaluate(t, ...);
}
// FIXED: Pass Type as a separate, annotated parameter
void Process(
object[] args,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type calleeType,
...) {
Evaluate(calleeType, ...); // annotation flows cleanly
}
会导致注解流中断的常见模式及其修复方法:
object[]参数包:将Type提取到单独的带注解参数中- Dictionary/List 存储:改用带注解的强类型字段
- 接口间接调用:为接口方法的参数添加注解
- 带装箱 getter 的属性:为属性的返回类型添加注解
策略 C:源生成 JSON 序列化(批量修复)
当大多数警告都是来自 JsonSerializer.Serialize/Deserialize 的 IL2026/IL3050 时,可以批量应用这一项机械式修复:
-
收集受影响的类型——搜索所有
JsonSerializer.Serialize和JsonSerializer.Deserialize调用点。提取要序列化的类型(Deserialize<T>中的<T>,或Serialize中对象的运行时类型)。 -
创建一个
JsonSerializerContext,并为找到的每种类型添加[JsonSerializable]。跳过外部包中的类型(例如,Azure.Core中的ResponseError)——无法为非自有类型生成源代码。请按照下方的注意事项 #1 单独处理外部类型。
[JsonSerializerContext]
[JsonSerializable(typeof(ManagedServiceIdentity))]
[JsonSerializable(typeof(SystemData))]
// ... one attribute per type YOU OWN
// Do NOT add types from external packages (e.g., ResponseError)
internal partial class MyProjectJsonContext : JsonSerializerContext { }
-
批量更新所有调用点——不要逐个读取文件。机械式地应用以下模式:
JsonSerializer.Serialize(obj)→JsonSerializer.Serialize(obj, MyProjectJsonContext.Default.TypeName)JsonSerializer.Deserialize<T>(json)→JsonSerializer.Deserialize(json, MyProjectJsonContext.Default.TypeName)
一次性查找并更新所有调用点:
# Find all files with JsonSerializer calls grep -rl 'JsonSerializer\.\(Serialize\|Deserialize\)' src/ --include='*.cs'然后使用连续的
edit调用,对每个匹配的文件应用相同的转换。不要对 C# 代码使用sed——Deserialize<T>()这类泛型调用包含尖括号和嵌套括号,sed会错误处理它们。 -
构建一次以进行验证。剩余警告将与序列化无关——请使用策略 A–B 或 D 处理。
策略 D:[RequiresUnreferencedCode](最后手段)
当某个方法从根本上需要无法静态描述的任意反射时:
[RequiresUnreferencedCode("Loads plugins by name using Assembly.Load")]
public void LoadPlugin(string assemblyName) {
var asm = Assembly.Load(assemblyName);
// ...
}
此属性会传递到调用方——调用方也必须添加 [RequiresUnreferencedCode]。请谨慎使用;它会将整个调用链标记为与裁剪不兼容。
步骤 5:重新构建并重复
每修复一小批警告(5-10 个)后,使用 --no-incremental 重新构建并检查是否出现新警告。不要尝试在重新构建前修复所有警告——频繁构建可以及早发现错误并揭示级联警告。修复会产生级联影响——为内部方法添加注解可能会导致其调用方出现警告。重复此过程,直到 0 Warning(s)。
步骤 6:验证所有 TFM
构建所有目标框架,以确保:
- net8.0 及更高版本的 TFM 上有 0 个 IL 警告
- 没有 NETSDK1210 警告(
IsAotCompatible条件会处理此问题) - 较旧 TFM(netstandard2.0、net472 等)上的构建干净
dotnet build <project.csproj> # builds all TFMs
停止信号
- 每种警告模式最多分析 2-3 个有代表性的文件。 找到某种模式的修复方法后,无需先逐个阅读所有文件,直接将其应用到所有匹配的文件。
- 完成一次构建后就开始修复。 不要进行第二轮分析;在步骤 3 的分类完成后,立即着手修复最常见的警告模式。
- net8.0+ TFM 达到 0 条 IL 警告后就停止。不要优化或重构已经没有警告的注解。
- 如果某条警告需要进行超出注解流修复范围的架构重构(例如,替换整个序列化层),请记录并停止,不要重写大型子系统。
- 对每条警告最多进行 3 轮构建-修复。如果 3 次尝试后注解流仍未解决问题,则改用
[RequiresUnreferencedCode]。 - 不要追查无法修改的第三方依赖项中的警告。记录下来后继续处理。
- 如果用户提出了范围明确的问题(例如“修复此文件中的警告”),不要将范围扩大到整个项目。
旧版 TFM 的 Polyfill
对于包含 netstandard2.0 或 net472 的多目标项目,需要为 DynamicallyAccessedMembersAttribute 及相关类型添加 polyfill。请参阅 references/polyfills.md。
常见注意事项
- 不具备 AOT 安全序列化能力的外部类型:如果某个类型来自你无法修改的依赖项(例如
Azure.Core中的ResponseError),且该类型没有源生成的序列化器,那么Options.GetConverter<T>()会使用基于反射的方式,并产生 IL 警告。首先检查该类型是否实现了IJsonModel<T>(Azure SDK 中很常见);如果是,则完全绕过JsonSerializer:
// Before (IL2026 — JsonSerializer uses reflection):
JsonSerializer.Serialize(writer, errorValue);
// After (AOT-safe — uses IJsonModel directly):
((IJsonModel<ResponseError>)errorValue).Write(writer, ModelReaderWriterOptions.Json);
// For deserialization:
var error = ((IJsonModel<ResponseError>)new ResponseError()).Create(ref reader, ModelReaderWriterOptions.Json);
不要将外部类型添加到 JsonSerializerContext 中——它不会为你不拥有的类型生成源代码。如果该类型未实现 IJsonModel<T>,请编写一个自定义 JsonConverter<T>,使用手动的 Utf8JsonReader/Utf8JsonWriter 逻辑,并通过上下文中的 [JsonSourceGenerationOptions] 注册它。
-
序列化库:大多数基于反射的序列化器(例如
Newtonsoft.Json、XmlSerializer)都不兼容 AOT。请迁移到基于源生成的序列化器,例如配合JsonSerializerContext使用System.Text.Json。如果无法迁移,请在序列化调用点标记[RequiresUnreferencedCode]。 -
共享项目 / projitems:通过
<Import>在多个项目之间共享源代码时,添加到共享代码中的注解会影响所有使用该代码的项目。请确认所有使用方仍能顺利构建。
参考资料
检查清单
- 已在 .csproj 中添加带 TFM 条件的
<IsAotCompatible> - 已启用 AOT 分析器进行构建(net8.0+ TFM)
- 已通过注解或重构修复所有 IL 警告
- 未对任何 IL 警告使用
#pragma warning disable或[UnconditionalSuppressMessage] - 如有需要,已为旧版 TFM 添加 polyfill
- 所有目标框架均已实现零警告构建
- 已确认共享 / 链接的源代码不会导致同级项目出错