mcpField
D
dotnet-trimming

dotnet-trimming

github.com/wshaddix开发8672 次安装35 周前更新
代码仓库

关于 dotnet-trimming

帮助开发者让 .NET 8+ 应用和库兼容裁剪的技能,涵盖注解、链接器描述文件、警告诊断及裁剪后输出测试。

主要功能

  1. 01使用裁剪注解
  2. 02通过 ILLink 描述文件保留类型
  3. 03查看详细链接器警告
  4. 04测试发布后的裁剪应用
  5. 05声明库兼容裁剪

适用场景

  1. 01解决反射相关的裁剪警告
  2. 02检查应用裁剪后的行为
  3. 03为裁剪环境中的使用者准备库
  4. 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]
IL2060MakeGenericType 调用使用了未知类型使用具体的泛型实例化
IL2062传递给 [DynamicallyAccessedMembers] 参数的值没有注解为来源添加 [DynamicallyAccessedMembers]
IL2067[DynamicallyAccessedMembers] 的参数不匹配确保注解在调用链中正确传递
IL2070Type.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 安全性
  1. 在库的 .csproj 中设置 <IsTrimmable>true</IsTrimmable>
  2. 使用 [RequiresUnreferencedCode] 为所有使用反射的 API 添加注解
  3. 为接收通过反射使用的类型的参数添加 [DynamicallyAccessedMembers]
  4. 尽可能用源代码生成器替代反射
  5. 通过在经过 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 注意事项

  1. 不要在库项目中使用 PublishTrimmed。 库使用 IsTrimmable 声明自身具备 Trim 安全性。PublishTrimmed 用于应用程序。
  2. 不要使用 RD.xml 保留类型。 RD.xml 是 .NET Native/UWP 格式,现代 .NET Trim 会静默忽略它。请改用 ILLink 描述符 XML 文件。
  3. 未经安全性验证,不要抑制 Trim 警告。 [UnconditionalSuppressMessage] 只会隐藏警告,无法解决根本问题。只有在确认代码安全时(例如,通过 ILLink 描述符验证后)才应抑制警告。
  4. 调试 Trim 问题时,不要忘记设置 TrimmerSingleWarn=false。 否则,每个程序集只会显示一条摘要警告,无法找到具体的问题调用位置。
  5. 不要混淆 IsTrimmable 和 PublishTrimmed。 IsTrimmable 用于声明库具备 Trim 安全性并启用分析器。PublishTrimmed 用于在应用程序中启用链接器。两者用途不同。
  6. 不要给不使用反射的方法添加 [RequiresUnreferencedCode]。 该注解会向调用方传递警告,调用方也必须添加注解或抑制警告。只有实际使用 Trim 不安全反射的方法才应添加注解。

参考资料

评论

支持 Markdown