关于 dotnet-trimming
帮助开发者让 .NET 8+ 应用和库兼容裁剪的技能,涵盖注解、链接器描述文件、警告诊断及裁剪后输出测试。
主要功能
- 01使用裁剪注解
- 02通过 ILLink 描述文件保留类型
- 03查看详细链接器警告
- 04测试发布后的裁剪应用
- 05声明库兼容裁剪
适用场景
- 01解决反射相关的裁剪警告
- 02检查应用裁剪后的行为
- 03为裁剪环境中的使用者准备库
- 04在 CI 中添加裁剪警告检查
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-trimming安装后在 Claude Code 中输入/dotnet-trimming确认已加载。
详细介绍
dotnet-trimming
面向 .NET 8+ 应用和库的修剪安全开发:修剪注解([RequiresUnreferencedCode]、[DynamicallyAccessedMembers]、[DynamicDependency])、用于保留类型的 ILLink 描述符 XML、用于细化诊断的 TrimmerSingleWarn、测试修剪后的输出、修复 IL2xxx/IL3xxx 警告,以及通过 IsTrimmable 编写库。
**版本假设:**以 .NET 8.0+ 为基准。修剪功能于 .NET 6 推出,但 .NET 8 提供了最完整的注解集和分析器覆盖范围。.NET 9 提高了警告准确性和库兼容性。
**不在范围内:**Native 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]。
交叉引用:[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>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>
PublishTrimmed 会指示链接器在发布时移除不可达代码。EnableTrimAnalyzer 会启用 Roslyn 分析器,在开发期间对不适合修剪的模式发出警告。
库
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
**关键区别:**库不设置 PublishTrimmed,因为它们不会作为独立应用发布。IsTrimmable 会告知使用者,该库的公共 API 已添加修剪安全注解。设置 IsTrimmable 会自动为库项目启用修剪分析器。
| 属性 | 项目类型 | 效果 |
|---|---|---|
PublishTrimmed | 应用 | 发布时进行修剪,并启用链接器 |
EnableTrimAnalyzer | 应用 | 在生成期间启用修剪警告 |
IsTrimmable | 库 | 声明修剪安全,并自动启用分析器 |
IsAotCompatible | 库 | 声明 AOT 安全,并自动启用 AOT 分析器 |
PublishAot | 应用 | 启用 AOT(隐含 PublishTrimmed) |
修剪注解
.NET 提供了用于注解与反射交互的代码的特性,帮助修剪器了解需要保留哪些内容。
[RequiresUnreferencedCode]
将方法标记为不适合修剪。修剪器和分析器在从修剪安全代码调用此方法时会生成 IL2026 警告。
[RequiresUnreferencedCode("Uses reflection to discover plugins")]
public IPlugin LoadPlugin(string typeName)
{
var type = Type.GetType(typeName)
?? throw new InvalidOperationException($"Type {typeName} not found");
return (IPlugin)Activator.CreateInstance(type)!;
}
[DynamicallyAccessedMembers]
告知修剪器通过反射访问类型的哪些成员,以便保留这些成员:
public T CreateInstance<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
where T : class
=> (T)Activator.CreateInstance(typeof(T))!;
// The trimmer preserves public constructors of T
// because the constraint tells it what's needed
[DynamicDependency]
显式保留特定成员,使其不被修剪:
// Preserve a method that is only called via reflection
[DynamicDependency(nameof(OnConfigChanged), typeof(ConfigWatcher))]
public void StartWatching() { /* reflects on OnConfigChanged */ }
// Preserve all public properties (e.g., for serialization)
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties,
typeof(LegacyDto))]
public void SerializeLegacy(LegacyDto dto) { /* ... */ }
[UnconditionalSuppressMessage]
当你已确认代码安全,但分析器仍对此提出警告时,可用它来抑制特定的修剪警告:
[UnconditionalSuppressMessage("Trimming",
"IL2026:RequiresUnreferencedCode",
Justification = "Type is preserved via ILLink descriptor")]
public void CallLegacyCode() { /* ... */ }
请谨慎使用——仅在通过 ILLink 描述符或其他方式确认安全后使用。
ILLink 描述符
ILLink 描述符 XML 文件会告知修剪器保留类型、方法或整个程序集。不要使用旧版 RD.xml——它是 .NET Native/UWP 格式,现代 .NET 修剪会静默忽略它。
描述符格式
<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>
注册
<ItemGroup>
<TrimmerRootDescriptor Include="ILLink.Descriptors.xml" />
</ItemGroup>
替代方案:TrimmerRootAssembly
对于不得修剪的整个程序集:
<ItemGroup>
<TrimmerRootAssembly Include="LegacyLibrary" />
</ItemGroup>
TrimmerSingleWarn
默认情况下,修剪器会按程序集汇总警告,并显示一行摘要。TrimmerSingleWarn=false 会显示每一条警告,这对于修复修剪问题至关重要。
# Default: one warning per assembly (hard to debug)
dotnet publish -c Release /p:PublishTrimmed=true
# warning IL2104: Assembly 'MyApp' produced trim warnings
# Detailed: per-occurrence warnings (easier to fix)
dotnet publish -c Release /p:PublishTrimmed=true /p:TrimmerSingleWarn=false
# warning IL2026: MyApp.PluginLoader.LoadPlugin(...) requires unreferenced code
# warning IL2057: Unrecognized value passed to Type.GetType(...)
# Analysis without publishing
dotnet build /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false
IL2xxx/IL3xxx 警告参考
Trim 警告(IL2xxx)
| 代码 | 含义 | 修复方法 |
|---|---|---|
| IL2026 | 方法具有 [RequiresUnreferencedCode] | 替换为 trim 安全的替代方案,或添加描述符 |
| IL2046 | 重写方法上的 Trim 特性不匹配 | 与基类型中的注解保持一致 |
| IL2057 | 无法识别的 Type.GetType() 参数 | 使用编译时已知的类型,或使用 [DynamicDependency] |
| IL2060 | MakeGenericType 调用使用了未知类型 | 使用具体的泛型实例化 |
| IL2062 | 传递给 [DynamicallyAccessedMembers] 参数的值没有注解 | 为来源添加 [DynamicallyAccessedMembers] |
| IL2067 | [DynamicallyAccessedMembers] 的参数不匹配 | 确保注解在调用链中正确传递 |
| IL2070 | Type.GetProperties() 等方法的 this 参数未添加注解 | 添加 [DynamicallyAccessedMembers] 约束 |
| IL2072 | 方法的返回值未添加注解 | 使用 [DynamicallyAccessedMembers] 为返回类型添加注解 |
| IL2104 | 程序集产生了 Trim 警告(摘要) | 使用 TrimmerSingleWarn=false 查看详细信息 |
AOT 警告(IL3xxx)
| 代码 | 含义 | 修复方法 |
|---|---|---|
| IL3050 | 方法具有 [RequiresDynamicCode] | 替换为源代码生成或静态替代方案 |
| IL3051 | [RequiresDynamicCode] 注解不匹配 | 与基类型中的注解保持一致 |
| IL3052 | 使用动态代码的 COM 互操作 | 使用带静态封送的 [LibraryImport] |
测试 Trim 后的输出
发布并测试
# Publish with trimming
dotnet publish -c Release -r linux-x64 /p:PublishTrimmed=true
# Run the trimmed binary
./bin/Release/net8.0/linux-x64/publish/MyApp
# Verify functionality:
# 1. All endpoints respond correctly
# 2. JSON deserialization produces populated objects
# 3. DI-resolved services function
# 4. No MissingMethodException or MissingMetadataException
在 CI 中测试 Trim
# CI script: publish trimmed and run integration tests
dotnet publish src/MyApp -c Release -r linux-x64 /p:PublishTrimmed=true -o ./publish
# Run smoke tests against trimmed binary
./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
Trim 警告 CI 门禁
# Fail CI if any trim warnings exist
dotnet build /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false \
/warnaserror:IL2026,IL2057,IL2060,IL2067,IL2070,IL3050
面向 Trim 的库创作
让库具备 Trim 安全性
- 在库的
.csproj中设置<IsTrimmable>true</IsTrimmable> - 使用
[RequiresUnreferencedCode]为所有使用反射的 API 添加注解 - 为接收通过反射使用的类型的参数添加
[DynamicallyAccessedMembers] - 尽可能用源代码生成器替代反射
- 通过在经过 Trim 的应用程序中使用该库进行测试
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
为公共 API 添加注解
// Method that uses reflection internally -- annotate honestly
[RequiresUnreferencedCode(
"Uses reflection to discover plugin types. " +
"Use RegisterPlugin<T>() for trim-safe plugin registration.")]
public IPlugin LoadPlugin(string typeName) { /* ... */ }
// Trim-safe alternative
public void RegisterPlugin<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] T>()
where T : class, IPlugin
{
_plugins[typeof(T).Name] = () => (IPlugin)Activator.CreateInstance<T>();
}
条件式 API
尽可能同时提供基于反射的 API 和 Trim 安全的 API:
public class ServiceRegistry
{
// Trim-safe: explicit type
public void Register<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicConstructors)] TService,
TImplementation>()
where TImplementation : class, TService
{ /* ... */ }
// Not trim-safe: assembly scanning
[RequiresUnreferencedCode("Scans assembly for service types")]
public void RegisterFromAssembly(Assembly assembly)
{ /* ... */ }
}
Agent 注意事项
- 不要在库项目中使用
PublishTrimmed。 库使用IsTrimmable声明自身具备 Trim 安全性。PublishTrimmed用于应用程序。 - 不要使用 RD.xml 保留类型。 RD.xml 是 .NET Native/UWP 格式,现代 .NET Trim 会静默忽略它。请改用 ILLink 描述符 XML 文件。
- 未经安全性验证,不要抑制 Trim 警告。
[UnconditionalSuppressMessage]只会隐藏警告,无法解决根本问题。只有在确认代码安全时(例如,通过 ILLink 描述符验证后)才应抑制警告。 - 调试 Trim 问题时,不要忘记设置
TrimmerSingleWarn=false。 否则,每个程序集只会显示一条摘要警告,无法找到具体的问题调用位置。 - 不要混淆
IsTrimmable和PublishTrimmed。IsTrimmable用于声明库具备 Trim 安全性并启用分析器。PublishTrimmed用于在应用程序中启用链接器。两者用途不同。 - 不要给不使用反射的方法添加
[RequiresUnreferencedCode]。 该注解会向调用方传递警告,调用方也必须添加注解或抑制警告。只有实际使用 Trim 不安全反射的方法才应添加注解。