.NET修剪技能 dotnet-trimming

这个技能专注于 .NET 8+ 应用程序和库的代码修剪技术,包括使用修剪注解(如 [RequiresUnreferencedCode]、[DynamicallyAccessedMembers])、ILLink 描述符 XML 文件、处理 IL2xxx/IL3xxx 警告、测试修剪输出以及库开发中的 IsTrimmable 设置。关键词:.NET 修剪、代码裁剪、ILLink、修剪注解、AOT 兼容、MSBuild 配置、DevOps 优化。

DevOps 0 次安装 0 次浏览 更新于 3/6/2026

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

库开发用于修剪

使库修剪安全

  1. 在库 .csproj 中设置 <IsTrimmable>true</IsTrimmable>
  2. [RequiresUnreferencedCode] 注解所有使用反射的 API
  3. 添加 [DynamicallyAccessedMembers] 到接收用于反射的类型的参数
  4. 尽可能用源代码生成器替换反射
  5. 通过从修剪应用消费库来测试
<!-- 库 .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)
    { /* ... */ }
}

代理注意事项

  1. 不要在库项目中使用 PublishTrimmed 库使用 IsTrimmable 声明它们是修剪安全的。PublishTrimmed 用于应用。
  2. 不要使用 RD.xml 进行类型保留。 RD.xml 是 .NET Native/UWP 格式,现代 .NET 修剪会静默忽略。使用 ILLink 描述符 XML 文件代替。
  3. 不要未验证安全性就抑制修剪警告。 [UnconditionalSuppressMessage] 隐藏警告但不修复根本问题。仅当您已验证代码安全(例如,通过 ILLink 描述符)时抑制。
  4. 调试修剪问题时不要忘记 TrimmerSingleWarn=false 没有它,您会得到每个程序集一行摘要警告,无法找到特定问题调用站点。
  5. 不要混淆 IsTrimmablePublishTrimmed IsTrimmable 声明库是修剪安全并启用分析器。PublishTrimmed 在应用中启用链接器。它们目的不同。
  6. 不要向不使用反射的方法添加 [RequiresUnreferencedCode] 注解病毒式传播 —— 调用者也必须被注解或抑制警告。仅注解实际使用修剪不安全反射的方法。

参考