mcpField
D
dotnet-aot-compat

dotnet-aot-compat

github.com/dotnet开发5.8k3.7k 次安装1 天前更新
代码仓库

关于 dotnet-aot-compat

用于处理分析器警告、使目标为 .NET 8 或更高版本的项目兼容 Native AOT 和裁剪的技能。它以构建结果驱动排查,优先保留注解传递或重构,而非抑制警告。

主要功能

  1. 01启用 AOT 与裁剪分析
  2. 02按警告代码分类问题
  3. 03添加反射注解或重构调用
  4. 04使用源生成 JSON 序列化
  5. 05验证多个目标框架构建

适用场景

  1. 01修复 IL 裁剪与 AOT 警告
  2. 02为反射代码补充注解
  3. 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 时,可以批量应用这一项机械式修复:

  1. 收集受影响的类型——搜索所有 JsonSerializer.Serialize 和 JsonSerializer.Deserialize 调用点。提取要序列化的类型(Deserialize<T> 中的 <T>,或 Serialize 中对象的运行时类型)。

  2. 创建一个 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 { }
  1. 批量更新所有调用点——不要逐个读取文件。机械式地应用以下模式:

    • 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 会错误处理它们。

  2. 构建一次以进行验证。剩余警告将与序列化无关——请使用策略 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。

常见注意事项

  1. 不具备 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] 注册它。

  1. 序列化库:大多数基于反射的序列化器(例如 Newtonsoft.Json、XmlSerializer)都不兼容 AOT。请迁移到基于源生成的序列化器,例如配合 JsonSerializerContext 使用 System.Text.Json。如果无法迁移,请在序列化调用点标记 [RequiresUnreferencedCode]。

  2. 共享项目 / projitems:通过 <Import> 在多个项目之间共享源代码时,添加到共享代码中的注解会影响所有使用该代码的项目。请确认所有使用方仍能顺利构建。

参考资料

限制 概念:了解裁剪 操作指南:裁剪兼容性

检查清单

  • 已在 .csproj 中添加带 TFM 条件的 <IsAotCompatible>
  • 已启用 AOT 分析器进行构建(net8.0+ TFM)
  • 已通过注解或重构修复所有 IL 警告
  • 未对任何 IL 警告使用 #pragma warning disable 或 [UnconditionalSuppressMessage]
  • 如有需要,已为旧版 TFM 添加 polyfill
  • 所有目标框架均已实现零警告构建
  • 已确认共享 / 链接的源代码不会导致同级项目出错

评论

支持 Markdown