关于 aot-trimming
aot-trimming 是面向 .NET 开发者的参考技能,帮助库和应用兼容裁剪与 Native AOT。它说明链接器注解、运行时反射的替代方案,以及通过发布构建检查警告的流程。
主要功能
- 01解释裁剪与 Native AOT 的行为
- 02指导使用链接器注解
- 03介绍反射安全的实现模式
- 04说明源生成器和动态代码边界
- 05提供发布与警告验证清单
适用场景
- 01使 .NET 库兼容裁剪
- 02解决 IL2xxx 或 IL3xxx 警告
- 03审查代码库的 AOT 兼容性
- 04用编译期方案替代运行时反射
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ aaronontheweb/ dotnet-skills --skill aot-trimming安装后在 Claude Code 中输入/aot-trimming确认已加载。
附带文件aot-trimming-playbook-reference.mdtrimming-attributes-reference.md
详细介绍
.NET 裁剪与 Native AOT
适用场景
- 为库或应用添加裁剪(
PublishTrimmed)或 Native AOT(PublishAot)支持 - 解决
IL2xxx(裁剪)和IL3xxx(AOT/单文件)警告 - 让大量使用反射的代码能够安全地进行裁剪
- 用编译时替代方案取代运行时反射、程序集扫描或
Reflection.Emit - 审查代码库或 PR 的裁剪/AOT 兼容性,包括生成的拦截器和分析器抑制器
- 设计必须携带裁剪注解的公共 API
核心目标
- 生成能够通过链接器可达性分析并在 Native AOT(无 JIT)下运行的代码。
- 使用裁剪属性说明编译器无法获知的信息——反射需要哪些成员。
- 将裁剪安全代码与不安全代码分开,使不安全路径明确且范围受限。
核心模型
裁剪与 Native AOT
- 裁剪(
PublishTrimmed)会运行 ILLink 可达性分析:仅保留静态可达的代码。分析器无法识别的反射调用所需代码会被裁剪掉,并产生警告。 - Native AOT(
PublishAot)要求必须进行裁剪,并移除 JIT。Reflection.Emit不受支持,运行时构造未知泛型实例无法保证可行,Expression.Compile()可能会回退到解释执行,而反射只能访问链接器保留的成员。 - 两者运行相同的静态分析:
IL2xxx— 裁剪警告:RequiresUnreferencedCode传播 +DynamicallyAccessedMembers数据流分析。IL3xxx— AOT 和单文件警告:RequiresDynamicCode/RequiresAssemblyFiles。
库可以支持裁剪,但不兼容 AOT(它使用 Reflection.Emit,裁剪可以容忍,但 AOT 不支持)。
需要解决的两个问题
- 无法识别的反射调用(裁剪):链接器会移除通过名称加载或在泛型方法中通过
typeof(T)访问的成员或类型。 - 动态代码生成(AOT):JIT 已移除,因此不支持
Reflection.Emit,并且无法保证能够构造未知泛型实例。
解决这两个问题的方法相同:让分析器能够静态识别所需成员,或者彻底消除对反射的需求。
反射并非问题所在
反射是一种合理的技术,在普通的 JIT 应用中完全没有问题——这类应用不需要这些措施。裁剪属性旨在允许在裁剪/AOT 环境中使用反射,而非禁止反射:为反射所需内容添加注解,链接器就会保留它们。只有当你希望移除或限制特定的反射接口时(例如,不想让公共注册 API 带有 [RequiresUnreferencedCode] 一起发布),才应考虑源生成器、[UnsafeAccessor] 或能力门控。
项目配置
对于库,声明兼容性,以便分析器报告警告,并让使用者了解:
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
</PropertyGroup>
对于应用,发布裁剪版本或 AOT 版本:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<PublishAot>true</PublishAot>
</PropertyGroup>
务必通过实际发布进行验证——裁剪/AOT 应用必须不产生任何警告:
dotnet publish -c Release -r <rid> -p:PublishTrimmed=true
dotnet publish -c Release -r <rid> -p:PublishAot=true
以库为根目录的测试应用是验证库的标准做法:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
<TrimmerRootAssembly Include="MyLibrary" />
</ItemGroup>
完整的属性表、功能开关和警告批准基线模式,请参阅 aot-trimming-playbook-reference.md。
特性模型
这些特性位于 System.Diagnostics.CodeAnalysis 中。完整目录及精确签名、警告代码和规则,请参阅 trimming-attributes-reference.md。
[RequiresUnreferencedCode(message)]— 标记需要链接器无法看到的成员的代码。在代码内部抑制警告;在每个调用点发出IL2026。[RequiresDynamicCode(message)]— 标记需要 JIT(发出代码、动态生成)的代码。在调用点发出IL3050。[DynamicallyAccessedMembers(memberTypes)]— 声明必须保留Type/string的哪些成员。它会从反射位置向后流向Type源。可将此特性(以及其他特性)直接放在访问器上,或使用[method:]、[field:]或[return:]目标,将其作用域限定为单个访问器。[UnconditionalSuppressMessage(category, checkId, Justification = "...")]— 最后的补救手段,抑制信息会持久化到 IL 中;必须附带理由和真实的不变量。[DynamicDependency("Member", typeof(T))]— 保留指定名称的成员,但不会抑制警告。
针对实际执行的反射,选择最窄的 DynamicallyAccessedMemberTypes:
| 反射 | 成员类型 |
|---|---|
Activator.CreateInstance<T>()、new T()、Activator.CreateInstance(type) | PublicParameterlessConstructor |
DI 激活(ActivatorUtilities.CreateFactory<T>) | PublicConstructors |
| 显式激活非公共成员 | NonPublicConstructors |
type.GetInterfaces() | Interfaces |
type.GetMethods() | PublicMethods(需要时添加 NonPublicMethods) |
type.GetProperties() | PublicProperties |
type.GetFields() | PublicFields |
type.GetNestedTypes() | PublicNestedTypes |
模式指南(节选)
首先判断代码是否真的需要兼容裁剪/AOT。普通的 JIT 应用或库无需考虑这些问题——反射在其中完全可用。只有当你或使用者发布裁剪/AOT 版本时,才需要让链接器能够识别反射。
目标绝不是“移除所有反射”,而是让反射在静态分析中可见,以便链接器保留所需内容。请针对每个调用点选择合适的方式:
-
保留反射并添加注解——最常见的情况。在
Type/string来源上添加[DynamicallyAccessedMembers(...)],并沿调用链传递;在入口点标记[RequiresUnreferencedCode]/[RequiresDynamicCode]。这些属性正是为了让反射在裁剪场景下仍能正常工作。请参阅 trimming-attributes-reference.md。 -
用编译时替代方案取代反射——当你希望移除反射接口时(例如,不希望公开注册 API 带有
[RequiresUnreferencedCode])。源生成器会生成直接的类型引用(标记属性 → 生成的注册代码);[UnsafeAccessor]可取代FieldInfo/MethodInfo/ConstructorInfo。请参阅 aot-trimming-playbook-reference.md 和 aot-trimming-playbook-reference.md。 -
隔离整个动态路径——当某条路径本质上是动态的(生成代理、按名称加载插件)时,将其置于边界之后,并通过
RuntimeFeature.IsDynamicCodeSupported选择兼容裁剪的替代方案。请参阅 aot-trimming-playbook-reference.md。 -
仅在万不得已时抑制警告——在已证明不变量成立的叶节点处添加一个
[UnconditionalSuppressMessage(...)],并提供Justification。切勿使用#pragma(它不会保留在 IL 中)。
请参阅 aot-trimming-playbook-reference.md,了解完整的模式指南,其中包括 object 重载 → 泛型重载的重定向、生成的拦截抑制器、有意设置的扫描边界、迁移分析器、严格模式优先级,以及临时警告批准基线。
参考文件
- trimming-attributes-reference.md:完整的属性目录——
RequiresUnreferencedCode、RequiresDynamicCode、DynamicallyAccessedMembers(完整的DynamicallyAccessedMemberTypes列表)、UnconditionalSuppressMessage、DynamicDependency——每项都包含用途、精确签名和规则,以及IL2xxx/IL3xxx警告代码表。 - aot-trimming-playbook-reference.md:完整的模式指南——源生成器与反射、兼容裁剪的隔离区和功能保护、拆分安全/不安全代码、System.Text.Json 源生成、使用
[OverloadResolutionPriority]和选择加入分析器将 object 重载 → 泛型重载重定向、迁移分析器、警告批准基线、已知陷阱,以及完整的生成检查清单。
生成检查清单(摘要)
- 项目 — 库设置
IsTrimmable/IsAotCompatible;应用设置PublishTrimmed/PublishAot;发布时确保零警告。 - 显式标注反射(或替换反射) — 为保留的反射添加标注(
DynamicallyAccessedMembers/RequiresUnreferencedCode/RequiresDynamicCode);仅在希望彻底移除反射接口时,才用源生成器、泛型(typeof(T))API 或[UnsafeAccessor]替换。 - 添加标注 — 对每个
Type/string来源使用范围最窄的DynamicallyAccessedMembers;沿调用链传递;在不安全入口点添加[RequiresUnreferencedCode]/[RequiresDynamicCode]。 - 隔离 — 将不安全的反射/发出代码放在边界后;使用
RuntimeFeature.IsDynamicCodeSupported为动态代码路径添加门控,并通过#if或标注分离修剪路径。 - 禁止警告 — 仅在已证明不变量成立的叶节点使用
[UnconditionalSuppressMessage]并提供理由;绝不使用#pragma/SuppressMessage。 - 验证 — 发布修剪版和 AOT 版,运行分析器,并使用批准基线锁定警告范围。