name: dotnet-trimming description: “修剪 .NET 8+ 应用和库。注解、ILLink 描述符、IL2xxx 警告、IsTrimmable。” user-invocable: false
dotnet-trimming
.NET 8+ 应用程序和库的修剪安全开发:修剪注解([RequiresUnreferencedCode]、[DynamicallyAccessedMembers]、[DynamicDependency]),用于类型保留的 ILLink 描述符 XML,TrimmerSingleWarn 用于精细诊断,测试修剪输出,修复 IL2xxx/IL3xxx 警告,以及使用 IsTrimmable 进行库开发。
版本假设: .NET 8.0+ 基线。修剪在 .NET 6 中推出,但 .NET 8 提供了最完整的注解表面和分析器覆盖。.NET 9 改进了警告准确性和库兼容性。
范围
- 用于修剪的 MSBuild 属性(应用与库)
- 修剪注解(RequiresUnreferencedCode、DynamicallyAccessedMembers、DynamicDependency)
- 用于类型保留的 ILLink 描述符 XML
- 用于精细诊断的 TrimmerSingleWarn
- IL2xxx/IL3xxx 警告参考和修复
- 测试修剪输出和 CI 门控
- 使用 IsTrimmable 和 IsAotCompatible 进行库开发
超出范围
- 原生 AOT 发布管道和 MSBuild 配置 —— 参见 [skill:dotnet-native-aot]
- AOT 优先设计模式 —— 参见 [skill:dotnet-aot-architecture]
- WASM AOT 编译 —— 参见 [skill:dotnet-aot-wasm]
- MAUI 特定 AOT 和修剪 —— 参见 [skill:dotnet-maui-aot]
- 源代码生成器开发 —— 参见 [skill:dotnet-csharp-source-generators]
- 序列化深度 —— 参见 [skill:dotnet-serialization]
- 容器部署 —— 参见 [skill:dotnet-containers]
- 性能模式(Span、池化) —— 参见 [skill:dotnet-performance-patterns]
交叉参考:[skill:dotnet-native-aot] 用于 AOT 编译管道,[skill:dotnet-aot-architecture] 用于 AOT 安全设计模式,[skill:dotnet-serialization] 用于 AOT 安全序列化,[skill:dotnet-csharp-source-generators] 用于源代码生成作为修剪启用器。
MSBuild 属性:应用与库
应用和库使用不同的 MSBuild 属性进行修剪。这一区别至关重要 —— 使用错误的属性会导致微妙问题。
对于应用
<PropertyGroup>
<!-- 在发布时启用修剪 -->
<PublishTrimmed>true</PublishTrimmed>
<!-- 在开发期间启用修剪分析器 -->
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<!-- 可选:如果面向 AOT,也启用 AOT 分析器 -->
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>
PublishTrimmed 告诉链接器在发布时移除不可达代码。EnableTrimAnalyzer 启用 Roslyn 分析器,在开发期间警告修剪不安全模式。
对于库
<PropertyGroup>
<!-- 声明库是修剪安全的(自动启用修剪分析器) -->
<IsTrimmable>true</IsTrimmable>
<!-- 声明 AOT 兼容性(自动启用 AOT 分析器) -->
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
关键区别: 库不设置 PublishTrimmed —— 它们不作为独立应用发布。IsTrimmable 告诉消费者库的公共 API 已注解为修剪安全。设置 IsTrimmable 会自动为库项目启用修剪分析器。
| 属性 | 项目类型 | 效果 |
|---|---|---|
PublishTrimmed |
应用 | 在发布时修剪,启用链接器 |
EnableTrimAnalyzer |
应用 | 在构建期间启用修剪警告 |
IsTrimmable |
库 | 声明修剪安全,自动启用分析器 |
IsAotCompatible |
库 | 声明 AOT 安全,自动启用 AOT 分析器 |
PublishAot |
应用 | 启用 AOT(隐含 PublishTrimmed) |
修剪注解
.NET 提供属性来注解与反射交互的代码,帮助修剪器理解要保留的内容。
[RequiresUnreferencedCode]
将方法标记为修剪不安全。当从修剪安全代码调用此方法时,修剪器和分析器会产生 IL2026 警告。
[RequiresUnreferencedCode("使用反射发现插件")]
public IPlugin LoadPlugin(string typeName)
{
var type = Type.GetType(typeName)
?? throw new InvalidOperationException($"类型 {typeName} 未找到");
return (IPlugin)Activator.CreateInstance(type)!;
}
[DynamicallyAccessedMembers]
告诉修剪器类型的哪些成员通过反射访问,以便保留它们:
public T CreateInstance<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
where T : class
=> (T)Activator.CreateInstance(typeof(T))!;
// 修剪器保留 T 的公共构造函数
// 因为约束告诉它需要什么
[DynamicDependency]
显式保留特定成员不被修剪:
// 保留仅通过反射调用的方法
[DynamicDependency(nameof(OnConfigChanged), typeof(ConfigWatcher))]
public void StartWatching() { /* 反射调用 OnConfigChanged */ }
// 保留所有公共属性(例如,用于序列化)
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties,
typeof(LegacyDto))]
public void SerializeLegacy(LegacyDto dto) { /* ... */ }
[UnconditionalSuppressMessage]
当您已验证代码安全尽管分析器有顾虑时,抑制特定修剪警告:
[UnconditionalSuppressMessage("Trimming",
"IL2026:RequiresUnreferencedCode",
Justification = "类型通过 ILLink 描述符保留")]
public void CallLegacyCode() { /* ... */ }
谨慎使用 —— 仅当您通过 ILLink 描述符或其他方式验证安全性时。
ILLink 描述符
ILLink 描述符 XML 文件告诉修剪器保留类型、方法或整个程序集。不要使用遗留的 RD.xml —— 它是 .NET Native/UWP 格式,现代 .NET 修剪会静默忽略。
描述符格式
<!-- ILLink.Descriptors.xml -->
<linker>
<!-- 保留特定类型 -->
<assembly fullname="MyApp">
<type fullname="MyApp.Models.PluginConfig" preserve="all" />
<type fullname="MyApp.Services.LegacyAdapter">
<method name="Initialize" />
<method name="ProcessRequest" />
</type>
</assembly>
<!-- 保留整个第三方程序集 -->
<assembly fullname="LegacyLibrary" preserve="all" />
</linker>
注册
<!-- 在 .csproj 中 -->
<ItemGroup>
<TrimmerRootDescriptor Include="ILLink.Descriptors.xml" />
</ItemGroup>
替代:TrimmerRootAssembly
对于必须完全不修剪的整个程序集:
<ItemGroup>
<!-- 保留整个程序集(完全不修剪) -->
<TrimmerRootAssembly Include="LegacyLibrary" />
</ItemGroup>
TrimmerSingleWarn
默认情况下,修剪器按程序集分组警告,显示一行摘要。TrimmerSingleWarn=false 显示每个单独警告,这对于修复修剪问题至关重要。
# 默认:每个程序集一个警告(难以调试)
dotnet publish -c Release /p:PublishTrimmed=true
# warning IL2104: 程序集 'MyApp' 产生修剪警告
# 详细:每次出现警告(更容易修复)
dotnet publish -c Release /p:PublishTrimmed=true /p:TrimmerSingleWarn=false
# warning IL2026: MyApp.PluginLoader.LoadPlugin(...) 需要未引用代码
# warning IL2057: 传递给 Type.GetType(...) 的未识别值
# 不发布的分析
dotnet build /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false
IL2xxx/IL3xxx 警告参考
修剪警告(IL2xxx)
| 代码 | 含义 | 修复 |
|---|---|---|
| IL2026 | 方法有 [RequiresUnreferencedCode] |
用修剪安全替代或添加描述符 |
| IL2046 | 覆盖上的修剪属性不匹配 | 匹配基类型的注解 |
| IL2057 | 未识别的 Type.GetType() 参数 |
使用编译时已知类型或 [DynamicDependency] |
| IL2060 | 带有未知类型的 MakeGenericType 调用 |
使用具体泛型实例化 |
| IL2062 | 传递给 [DynamicallyAccessedMembers] 参数的值无注解 |
添加 [DynamicallyAccessedMembers] 到源 |
| IL2067 | [DynamicallyAccessedMembers] 参数不匹配 |
确保注解通过调用链正确流动 |
| IL2070 | Type.GetProperties() 等的 this 参数未注解 |
添加 [DynamicallyAccessedMembers] 约束 |
| IL2072 | 方法返回值未注解 | 用 [DynamicallyAccessedMembers] 注解返回类型 |
| IL2104 | 程序集产生修剪警告(摘要) | 使用 TrimmerSingleWarn=false 获取详情 |
AOT 警告(IL3xxx)
| 代码 | 含义 | 修复 |
|---|---|---|
| IL3050 | 方法有 [RequiresDynamicCode] |
用源代码生成或静态替代替换 |
| IL3051 | [RequiresDynamicCode] 注解不匹配 |
匹配基类型的注解 |
| IL3052 | 带动态代码的 COM 互操作 | 使用带静态封送的 [LibraryImport] |
测试修剪输出
发布和测试
# 带修剪发布
dotnet publish -c Release -r linux-x64 /p:PublishTrimmed=true
# 运行修剪后的二进制文件
./bin/Release/net8.0/linux-x64/publish/MyApp
# 验证功能:
# 1. 所有端点正确响应
# 2. JSON 反序列化产生填充对象
# 3. DI 解析服务功能正常
# 4. 无 MissingMethodException 或 MissingMetadataException
CI 中的修剪测试
# CI 脚本:发布修剪并运行集成测试
dotnet publish src/MyApp -c Release -r linux-x64 /p:PublishTrimmed=true -o ./publish
# 对修剪后的二进制运行冒烟测试
./publish/MyApp &
APP_PID=$!
sleep 3
curl -f http://localhost:8080/health/live || (kill $APP_PID; exit 1)
curl -f http://localhost:8080/api/products || (kill $APP_PID; exit 1)
kill $APP_PID
修剪警告 CI 门控
# 如果存在任何修剪警告,CI 失败
dotnet build /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false \
/warnaserror:IL2026,IL2057,IL2060,IL2067,IL2070,IL3050
库开发用于修剪
使库修剪安全
- 在库
.csproj中设置<IsTrimmable>true</IsTrimmable> - 用
[RequiresUnreferencedCode]注解所有使用反射的 API - 添加
[DynamicallyAccessedMembers]到接收用于反射的类型的参数 - 尽可能用源代码生成器替换反射
- 通过从修剪应用消费库来测试
<!-- 库 .csproj -->
<PropertyGroup>
<!-- 自动启用修剪分析器 -->
<IsTrimmable>true</IsTrimmable>
<!-- 自动启用 AOT 分析器;在 .NET 8+ 中隐含 IsTrimmable -->
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
注解公共 API
// 内部使用反射的方法 —— 诚实注解
[RequiresUnreferencedCode(
"使用反射发现插件类型。" +
"使用 RegisterPlugin<T>() 进行修剪安全插件注册。")]
public IPlugin LoadPlugin(string typeName) { /* ... */ }
// 修剪安全替代
public void RegisterPlugin<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
where T : class, IPlugin
{
_plugins[typeof(T).Name] = () => (IPlugin)Activator.CreateInstance<T>();
}
条件 API
尽可能提供基于反射和修剪安全的 API:
public class ServiceRegistry
{
// 修剪安全:显式类型
public void Register<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] TService,
TImplementation>()
where TImplementation : class, TService
{ /* ... */ }
// 非修剪安全:程序集扫描
[RequiresUnreferencedCode("扫描程序集以查找服务类型")]
public void RegisterFromAssembly(Assembly assembly)
{ /* ... */ }
}
代理注意事项
- 不要在库项目中使用
PublishTrimmed。 库使用IsTrimmable声明它们是修剪安全的。PublishTrimmed用于应用。 - 不要使用 RD.xml 进行类型保留。 RD.xml 是 .NET Native/UWP 格式,现代 .NET 修剪会静默忽略。使用 ILLink 描述符 XML 文件代替。
- 不要未验证安全性就抑制修剪警告。
[UnconditionalSuppressMessage]隐藏警告但不修复根本问题。仅当您已验证代码安全(例如,通过 ILLink 描述符)时抑制。 - 调试修剪问题时不要忘记
TrimmerSingleWarn=false。 没有它,您会得到每个程序集一行摘要警告,无法找到特定问题调用站点。 - 不要混淆
IsTrimmable和PublishTrimmed。IsTrimmable声明库是修剪安全并启用分析器。PublishTrimmed在应用中启用链接器。它们目的不同。 - 不要向不使用反射的方法添加
[RequiresUnreferencedCode]。 注解病毒式传播 —— 调用者也必须被注解或抑制警告。仅注解实际使用修剪不安全反射的方法。