mcpField
D
dotnet-trimming

dotnet-trimming

github.com/wshaddix開発8672 回インストール35 週間前に更新
リポジトリ

dotnet-trimming について

.NET 8 以降のアプリとライブラリをトリミング対応にするためのスキルです。アノテーション、リンカーデスクリプター、警告の調査、トリミング後の出力テストを説明します。

主な機能

  1. 01トリミング用アノテーションの適用
  2. 02ILLink デスクリプターで型を保持
  3. 03リンカー警告の詳細確認
  4. 04トリミング後のアプリをテスト
  5. 05ライブラリのトリミング対応を宣言

活用シーン

  1. 01リフレクション関連の警告を解消
  2. 02トリミング後のアプリ動作を確認
  3. 03トリミング利用者向けにライブラリを準備
  4. 04CI にトリミング警告チェックを追加

インストール

コマンドをコピーしてターミナルで実行します。

$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

既定では、トリマーはアセンブリごとに警告をまとめ、概要を 1 行表示します。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 警告リファレンス

トリミングに関する警告(IL2xxx)
コード意味対処方法
IL2026メソッドに [RequiresUnreferencedCode] が指定されていますトリミング対応の代替手段に置き換えるか、記述子を追加します
IL2046オーバーライドのトリミング属性が一致していません基底型の注釈に合わせます
IL2057Type.GetType() の引数を認識できませんコンパイル時に型が判明している方法、または [DynamicDependency] を使用します
IL2060型が不明な状態で MakeGenericType が呼び出されています具体的なジェネリック型のインスタンス化を使用します
IL2062[DynamicallyAccessedMembers] パラメーターに渡された値に注釈がありませんソースに [DynamicallyAccessedMembers] を追加します
IL2067[DynamicallyAccessedMembers] のパラメーターが一致していません呼び出しチェーンを通じて注釈が正しく引き継がれることを確認します
IL2070Type.GetProperties() などの this パラメーターに注釈がありません[DynamicallyAccessedMembers] 制約を追加します
IL2072メソッドの戻り値に注釈がありません戻り値の型に [DynamicallyAccessedMembers] を指定します
IL2104アセンブリでトリミングに関する警告が発生しました(概要)詳細を確認するには TrimmerSingleWarn=false を使用します
AOT に関する警告(IL3xxx)
コード意味対処方法
IL3050メソッドに [RequiresDynamicCode] が指定されていますソースジェネレーターまたは静的な代替手段に置き換えます
IL3051[RequiresDynamicCode] の注釈が一致していません基底型の注釈に合わせます
IL3052動的コードを使用する COM 相互運用が含まれています静的マーシャリングを使用する [LibraryImport] を使用します

トリミング後の出力をテストする

発行してテストする
# 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 でトリミングをテストする
# 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
トリミング警告の CI ゲート
# Fail CI if any trim warnings exist
dotnet build /p:EnableTrimAnalyzer=true /p:TrimmerSingleWarn=false \
  /warnaserror:IL2026,IL2057,IL2060,IL2067,IL2070,IL3050

トリミングに対応したライブラリの作成

ライブラリをトリミング対応にする
  1. ライブラリの .csproj で <IsTrimmable>true</IsTrimmable> を設定します
  2. リフレクションを使用するすべての API に [RequiresUnreferencedCode] を指定します
  3. リフレクションで使用する型を受け取るパラメーターに [DynamicallyAccessedMembers] を追加します
  4. 可能な場合は、リフレクションをソースジェネレーターに置き換えます
  5. トリミングされたアプリケーションからライブラリを使用してテストします

<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 とトリミング対応 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)
    { /* ... */ }
}

エージェントが注意すべき点

  1. ライブラリプロジェクトで PublishTrimmed を使用しないでください。 ライブラリでは IsTrimmable を使用して、トリミング対応であることを宣言します。PublishTrimmed はアプリケーション用です。
  2. 型を保持するために RD.xml を使用しないでください。 RD.xml は .NET Native/UWP 形式であり、最新の .NET のトリミングでは無視されても警告されません。代わりに ILLink 記述子の XML ファイルを使用します。
  3. 安全性を確認せずにトリミング警告を抑制しないでください。 [UnconditionalSuppressMessage] は警告を隠しますが、根本的な問題は解決しません。コードが安全であることを確認した場合(ILLink 記述子を使用するなど)に限り、警告を抑制してください。
  4. トリミングの問題をデバッグする際は TrimmerSingleWarn=false を忘れないでください。 これを指定しないと、アセンブリごとに概要の警告が 1 つだけ表示され、問題のある呼び出し箇所を特定できなくなります。
  5. IsTrimmable と PublishTrimmed を混同しないでください。 IsTrimmable はライブラリがトリミング対応であることを宣言し、アナライザーを有効にします。PublishTrimmed はアプリケーションでリンカーを有効にします。これらの用途は異なります。
  6. リフレクションを使用しないメソッドに [RequiresUnreferencedCode] を追加しないでください。 この注釈は呼び出し元にも連鎖的に伝播するため、呼び出し元にも注釈を付けるか、警告を抑制する必要があります。トリミングに安全でないリフレクションを実際に使用するメソッドにのみ注釈を付けてください。

参考資料

コメント