⚡

.NET修剪技能Skill dotnet-trimming

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

DevOps 0 次安装 8 次浏览 更新于 3/6/2026
名称 dotnet-trimming
描述 “修剪 .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. 不要混淆 IsTrimmable 和 PublishTrimmed。 IsTrimmable 声明库是修剪安全并启用分析器。PublishTrimmed 在应用中启用链接器。它们目的不同。
  6. 不要向不使用反射的方法添加 [RequiresUnreferencedCode]。 注解病毒式传播 —— 调用者也必须被注解或抑制警告。仅注解实际使用修剪不安全反射的方法。

参考