用途
本技能用于为 .NET 库或应用添加裁剪(PublishTrimmed)与 Native AOT(PublishAot)支持。适用场景包括:解决 IL2xxx(裁剪)与 IL3xxx(AOT/单文件)警告;让大量使用反射的代码变为裁剪安全;用编译期方案替换运行时反射、程序集扫描或 Reflection.Emit;审查代码库或 PR 的裁剪/AOT 兼容性(含生成的拦截器与分析器抑制器);设计必须携带裁剪标注的公共 API。
核心目标
- 产出能通过链接器可达性分析、并可在 Native AOT(无 JIT)下运行的代码。
- 用裁剪特性表达编译器看不到的信息,即反射需要哪些成员。
- 将裁剪安全与不安全的代码分离,使不安全路径显式且受控。
核心模型
裁剪(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 下可用,而非禁止反射——为反射所需内容加标注,链接器就会保留它。仅当你希望移除或收拢某个特定反射面(例如不想随 [RequiresUnreferencedCode] 一起发布的公共注册 API)时,才考虑源生成器、[UnsafeAccessor] 或能力门控。
项目配置
库声明兼容性(让分析器暴露警告并让使用者知晓):
<PropertyGroup>
<!-- Trim-compatible: enables trim warnings. -->
<IsTrimmable>true</IsTrimmable>
<!-- AOT-compatible: implies IsTrimmable + EnableTrimAnalyzer + EnableSingleFileAnalyzer + EnableAotAnalyzer. -->
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
</PropertyGroup>
应用发布裁剪或 AOT:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<!-- or -->
<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。
- 用编译期替代方案替换反射——当你希望移除某个反射面时(例如不想随 [RequiresUnreferencedCode] 发布的公共注册 API)。源生成器生成直接类型引用(标记特性 → 生成注册);[UnsafeAccessor] 替代 FieldInfo/MethodInfo/ConstructorInfo。见 aot-trimming-playbook-reference.md。
- 隔离整条动态路径——当某条路径本质上就是动态的(生成代理、按名称加载插件),将其移到边界之后,并通过 RuntimeFeature.IsDynamicCodeSupported 提供裁剪安全的替代方案。
- 仅作为最后手段抑制——在已证明不变量的叶子处使用单个 [UnconditionalSuppressMessage(...)],并附 Justification。绝不要用 #pragma(不会持久化到 IL)。
完整手册见 aot-trimming-playbook-reference.md,包括对象重载 → 泛型重载重定向、生成拦截抑制器、有意的扫描边界、迁移分析器、严格模式优先级与临时警告批准基线。
参考资料文件
trimming-attributes-reference.md:完整特性目录——RequiresUnreferencedCode、RequiresDynamicCode、DynamicallyAccessedMembers(完整的 DynamicallyAccessedMemberTypes 列表)、UnconditionalSuppressMessage、DynamicDependency——各自包含意图、精确签名与规则,以及 IL2xxx/IL3xxx 警告代码表。
aot-trimming-playbook-reference.md:完整模式手册——源生成器与反射对比、裁剪安全孤岛与能力守卫、安全/不安全代码分离、System.Text.Json 源生成、使用 [OverloadResolutionPriority] 的对象重载 → 泛型重载重定向与可选分析器、迁移分析器、警告批准基线、已知陷阱与完整生成清单。
生成清单(摘要)
- 项目——库上使用 IsTrimmable/IsAotCompatible;应用上使用 PublishTrimmed/PublishAot;发布时零警告。
- 让反射可见(或替换它)——为保留的反射加标注(DynamicallyAccessedMembers/RequiresUnreferencedCode/RequiresDynamicCode);仅在你希望移除反射面时,用源生成器、泛型(typeof(T))API 或 [UnsafeAccessor] 替换。
- 标注——在每个 Type/string 来源上使用最窄的 DynamicallyAccessedMembers;沿调用链传播;在不安全入口点标记 [RequiresUnreferencedCode]/[RequiresDynamicCode]。
- 隔离——将不安全的反射/emit 代码置于边界之后;用 RuntimeFeature.IsDynamicCodeSupported 门控动态代码路径,用 #if 或标注分离裁剪路径。
- 抑制——仅在已证明不变量的叶子处使用 [UnconditionalSuppressMessage] + 理由;绝不用 #pragma/SuppressMessage。
- 验证——发布裁剪与 AOT 版本,运行分析器,并用批准基线锁定警告面。
参考链接
- Trimming options
- Prepare .NET libraries for trimming
- Introduction to trim warnings
- Native AOT deployment
- ILLink error codes