トリミング用に .NET ライブラリを準備する

.NET SDK を使用すると、トリミングによって自己完結型アプリのサイズを縮小できます。 トリミングにより、使用されていないコードをアプリとその依存関係から削除できます。 すべてのコードにトリミングとの互換性があるわけではありません。 .NET では、トリミングされたアプリを破損する可能性のあるパターンを検出するための、トリミング分析の警告を提供しています。 この記事の内容は次のとおりです。

前提条件

.NET 6 SDK 以降。

最新のトリミング警告とアナライザー カバレッジを取得するには、次のようにします。

  • .NET 8 SDK 以降をインストールし、使用します。
  • net8.0 以降をターゲットとします。

.NET 7 SDK 以降。

最新のトリミング警告とアナライザー カバレッジを取得するには、次のようにします。

  • .NET 8 SDK 以降をインストールし、使用します。
  • net8.0 以降をターゲットとします。

.NET 8 SDK 以降。

ライブラリのトリミングの警告を有効にする

ライブラリのトリミング警告は、以下のいずれかの方法で見つけることができます。

  • IsTrimmable プロパティを使用してプロジェクト固有のトリミングを有効にします。
  • ライブラリを使用するトリミング テスト アプリを作成し、テスト アプリのトリミングを有効にします。 ライブラリのすべての API を参照する必要はありません。

両方のアプローチの使用をお勧めします。 プロジェクト固有のトリミングは便利で、1 つのプロジェクトに対するトリミングの警告を表示できますが、すべての警告を表示するためにトリミング対応とマークされている参照に依存します。 テスト アプリのトリミングには手間がかかりますが、すべての警告が表示されます。

プロジェクト固有のトリミングを有効にする

プロジェクト ファイルで <IsTrimmable>true</IsTrimmable> を設定します。

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

MSBuild プロパティ IsTrimmabletrue に設定すると、アセンブリが "トリミング可能" とマークされ、トリミング警告が有効になります。 "トリミング可能" は、以下のプロジェクトを意味します。

  • トリミングとの互換性があると見なされます。
  • ビルド時にトリミング関連の警告が生成されないようにしてください。 トリミングされたアプリで使用されたとき、最終的な出力において、アセンブリではその未使用のメンバーがトリミングされます。

IsTrimmable プロパティは、AOT が <IsAotCompatible>true</IsAotCompatible> と互換性があるとしてプロジェクトを構成するときに、既定で true になります。 詳細については、「AOT 互換性アナライザー」を参照してください。

プロジェクトをトリミング互換としてマークせずにトリミング警告を生成するには、<IsTrimmable>true</IsTrimmable> ではなく <EnableTrimAnalyzer>true</EnableTrimAnalyzer> を使用します。

テスト アプリですべての警告を表示する

ライブラリのすべての解析警告を表示するには、ライブラリの実装と、ライブラリで使用するすべての依存関係の実装をトリマーで解析する必要があります。

以下のライブラリのビルドおよび公開時。

  • 依存関係の実装は使用できません。
  • 使用可能な参照アセンブリには、トリマーがトリミングに互換性があるかどうかを判断するのに十分な情報がありません。

依存関係に制限があるため、ライブラリとその依存関係を使用する自己完結型のテスト アプリを作成する必要があります。 テスト アプリには、トリマーがトリミングの非互換性について警告を発するために必要なすべての情報が含まれています。

  • ライブラリ コード。
  • ライブラリで依存関係から参照されるコード。

Note

ターゲット フレームワークに依存してライブラリの動作が異なる場合は、トリミングをサポートするターゲット フレームワークごとにトリミング テスト アプリを作成します。 たとえば、ライブラリが #if NET7_0 などの条件付きコンパイルを使用して動作を変える場合があります。

トリミング テスト アプリを作成するには、次のようにします。

  • 分離コンソール アプリケーション プロジェクトを作成します。
  • ライブラリへの参照を追加します。
  • 以下のリストを使用して、以下の図に示すようなプロジェクトと同様のプロジェクトを変更します。

net472netstandard2.0 のように、ライブラリでトリミングできない TFM がターゲットである場合、トリミング テスト アプリを作成するメリットはありません。 トリミングは .NET 6 以降でのみサポートされています。

  • <TrimmerDefaultAction>link に設定します。
  • <PublishTrimmed>true</PublishTrimmed>を追加します。
  • <ProjectReference Include="/Path/To/YourLibrary.csproj" /> を使用してライブラリ プロジェクトに参照を追加します。
  • <TrimmerRootAssembly Include="YourLibraryName" /> を使ってトリマー ルート アセンブリとしてライブラリを指定します。
    • TrimmerRootAssembly では、ライブラリのすべての部分が分析されるようになります。 これにより、トリマーにこのアセンブリが "ルート" であることを伝達します。 "ルート" アセンブリとは、トリマーがライブラリ内のすべての呼び出しを分析し、そのアセンブリを起点とするすべてのコード パスを横断することを意味します。
  • <PublishTrimmed>true</PublishTrimmed>を追加します。
  • <ProjectReference Include="/Path/To/YourLibrary.csproj" /> を使用してライブラリ プロジェクトに参照を追加します。
  • <TrimmerRootAssembly Include="YourLibraryName" /> を使ってトリマー ルート アセンブリとしてライブラリを指定します。
    • TrimmerRootAssembly では、ライブラリのすべての部分が分析されるようになります。 これにより、トリマーにこのアセンブリが "ルート" であることを伝達します。 "ルート" アセンブリとは、トリマーがライブラリ内のすべての呼び出しを分析し、そのアセンブリを起点とするすべてのコード パスを横断することを意味します。
  • <PublishTrimmed>true</PublishTrimmed>を追加します。
  • <ProjectReference Include="/Path/To/YourLibrary.csproj" /> を使用してライブラリ プロジェクトに参照を追加します。
  • <TrimmerRootAssembly Include="YourLibraryName" /> を使ってトリマー ルート アセンブリとしてライブラリを指定します。
    • TrimmerRootAssembly では、ライブラリのすべての部分が分析されるようになります。 これにより、トリマーにこのアセンブリが "ルート" であることを伝達します。 "ルート" アセンブリとは、トリマーがライブラリ内のすべての呼び出しを分析し、そのアセンブリを起点とするすべてのコード パスを横断することを意味します。

.csproj ファイル

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
    <!-- Prevent warnings from unused code in dependencies -->
    <TrimmerDefaultAction>link</TrimmerDefaultAction>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="path/to/MyLibrary.csproj" />
    <!-- Analyze the whole library, even if attributed with "IsTrimmable" -->
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>

注意: 以前のプロジェクト ファイルで .NET 7 を使用する場合、<TargetFramework>net8.0</TargetFramework><TargetFramework>net7.0</TargetFramework> に置き換えます。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>

プロジェクト ファイルが更新されたら、ターゲットのランタイム識別子 (RID) を使用して dotnet publish を実行します。

dotnet publish -c Release -r <RID>

複数のライブラリに対して前述のパターンに従ってください。 一度に複数のライブラリのトリミング分析の警告を表示するには、ProjectReference 項目および TrimmerRootAssembly 項目と同じプロジェクトにそのすべてを追加します。 すべてのライブラリを ProjectReference および TrimmerRootAssembly の項目で同じプロジェクトに追加すると、ルート ライブラリのいずれかで依存関係にトリミングに対応していない API が使用されている場合に、依存関係に関する警告が表示されます。 特定のライブラリだけに関係する警告を表示するには、そのライブラリのみを参照します。

注意: 分析結果は、依存関係の実装の詳細によって異なります。 依存関係を新しいバージョンに更新すると、解析に関する以下の警告が表示される場合があります。

  • 新しいバージョンに理解できない反射パターンが追加された場合。
  • API に変更がない場合でも同様です。
  • ライブラリが PublishTrimmed で使用されているときに、トリミングの分析警告を導入すると、破壊的変更になります。

トリミングの警告を解決する

前述の手順では、トリミングされたアプリで使用した場合に問題を引き起こす可能性のあるコードに関する警告が生成されます。 以下の例は、最も一般的な警告と、その警告を修正するための推奨事項を示しています。

RequiresUnreferencedCode

[RequiresUnreferencedCode] を使用し、System.Reflection 経由など、静的に参照されないコードへの動的アクセスが指定のメソッドに必要であることを示す次のコードを検討してください。

public class MyLibrary
{
    public static void MyMethod()
    {
        // warning IL2026 :
        // MyLibrary.MyMethod: Using 'MyLibrary.DynamicBehavior'
        // which has [RequiresUnreferencedCode] can break functionality
        // when trimming app code.
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

強調表示された次のコードは、トリミングと互換性がないと明示的に注釈が付けられているメソッドをライブラリが呼び出すことを意味します。 警告が表示されないようにするには、MyMethodDynamicBehavior を呼び出す必要があるかどうかを検討します。 その場合は、呼び出し元 MyMethod にも [RequiresUnreferencedCode] という注釈を付け、MyMethod の呼び出し元が代わりに警告を受け取るように警告を伝播します。

public class MyLibrary
{
    [RequiresUnreferencedCode("Calls DynamicBehavior.")]
    public static void MyMethod()
    {
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

パブリック API まで属性が伝搬したら、ライブラリを呼び出すアプリは以下のとおりです。

  • トリミングできないパブリック メソッドにのみ警告を発します。
  • IL2104: Assembly 'MyLibrary' produced trim warnings のような警告が表示されないようにしてください。

DynamicallyAccessedMembers

public class MyLibrary3
{
    static void UseMethods(Type type)
    {
        // warning IL2070: MyLibrary.UseMethods(Type): 'this' argument does not satisfy
        // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
        // 'System.Type.GetMethods()'.
        // The parameter 't' of method 'MyLibrary.UseMethods(Type)' doesn't have
        // matching annotations.
        foreach (var method in type.GetMethods())
        {
            // ...
        }
    }
}

前述のコードで、UseMethods は、[DynamicallyAccessedMembers] 要件が含まれるリフレクション メソッドを呼び出しています。 この要件は、型のパブリック メソッドが使用可能であることを示します。 UseMethods のパラメーターに同じ要件を追加することによって、要件を満たすことができます。

static void UseMethods(
   // State the requirement in the UseMethods parameter.
   [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ...
}

ここで、UseMethods を呼び出すと、PublicMethods 要件を満たさない値が渡された場合に警告が生成されるようになります。 [RequiresUnreferencedCode] と同様に、パブリック API にこのような警告が伝播したら、完了です。

以下の例では、未知の Type が注釈を付けたメソッドのパラメーターに流れ込んでいます。 不明の Type を以下のフィールドから取得します。

static Type type;
static void UseMethodsHelper()
{
    // warning IL2077: MyLibrary.UseMethodsHelper(Type): 'type' argument does not satisfy
    // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
    // 'MyLibrary.UseMethods(Type)'.
    // The field 'System.Type MyLibrary::type' does not have matching annotations.
    UseMethods(type);
}

ここでの問題も、フィールド type がこれらの要件があるパラメーターに渡されることです。 これは [DynamicallyAccessedMembers] をフィールドに追加することで修正されます。 [DynamicallyAccessedMembers] により、互換性のない値がフィールドに割り当てられるコードについての警告が表示されます。 このプロセスは、パブリック API に注釈が付けられるまで続行されます。また、具象型がこれらの要件がある場所にフローすると終了する場合もあります。 次に例を示します。

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;

static void UseMethodsHelper()
{
    MyLibrary.type = typeof(System.Tuple);
}

この場合、トリミング分析は Tuple のパブリック メソッドを保持し、それ以上の警告は生成しません。

推奨事項

  • 可能な場合はリフレクションを避けてください。 リフレクションを使用する場合は、ライブラリの小さな部分からのみ到達できるように、リフレクション範囲を最小化します。
  • 可能な場合は、トリミング要件を静的に表現するために DynamicallyAccessedMembers を使用してコードに注釈を付けます。
  • DynamicallyAccessedMembers を使用して注釈を付けられる分析可能なパターンに従うようにコードを再編成することを検討してください
  • コードにトリミングとの互換性がない場合、RequiresUnreferencedCode を使用して注釈を付け、関連するパブリック API に注釈が付けられるまで、この注釈を呼び出し元に伝播させます。
  • 静的解析で解釈できないような方法でリフレクションを使用するコードは使用しないでください。 たとえば、静的コンストラクターでのリフレクションは避けるべきです。 静的コンストラクターで静的に解析不可能なリフレクションを使用すると、警告がクラスのすべてのメンバーに伝搬します。
  • 仮想メソッドやインターフェイス メソッドに注釈を付けないでください。 仮想メソッドまたはインターフェイス メソッドに注釈を付けるには、すべてのオーバーライドで注釈の一致が必要です。
  • API にほとんどトリミング互換性がない場合、API に対する代替のコーディング アプローチを検討する必要がある場合があります。 一般的な例として、リフレクションベースのシリアライザーがあります。 このような場合は、ソース ジェネレーターなどの他のテクノロジを採用して、より簡単に静的に分析できるコードを生成することを検討してください。 たとえば、「System.Text.Json でソース生成を使用する方法」を参照してください

分析不可能なパターンの警告を解決する

可能な場合は、[RequiresUnreferencedCode]DynamicallyAccessedMembers を使用してコードの意図を表すことで、警告を解決することをお勧めします。 しかしながら、場合によっては、そのような属性では表現できないパターンを使用するライブラリのトリミングを有効にしたい場合や、既存のコードをリファクタリングしたくないことがあります。 このセクションでは、トリミング分析の警告を解決するための高度な方法についていくつか説明します。

警告

これらのテクニックは、使い方を誤ると、動作やコードを変更したり、実行時に例外が発生したりする可能性があります。

UnconditionalSuppressMessage

以下のコードを検討してください。

  • 注釈では表現できない意図。
  • 警告は表示されるが、実行時に実際の問題が発生することはない。

警告は UnconditionalSuppressMessageAttribute を抑制できません。 これは SuppressMessageAttribute に似ていますが、IL で永続化され、トリミング分析中に考慮されます。

警告

警告を抑制する場合は、検査とテストによって true であることがわかっている不変条件に基づいて、コードのトリミングの互換性を保証する責任があります。 これらの注釈は、正しくない場合、またはコードの不変条件が変更された場合に、誤ったコードがわからなくなる可能性があるため、注意してください。

次に例を示します。

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        // warning IL2063: TypeCollection.Item.get: Value returned from method
        // 'TypeCollection.Item.get' can't be statically determined and may not meet
        // 'DynamicallyAccessedMembersAttribute' requirements.
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

前述のコードでは、返された TypeCreateInstance の要件と合うように、インデクサー プロパティに注釈を付けています。 これにより、TypeWithConstructor コンストラクターが保持され、CreateInstance の呼び出しで警告が表示されなくなります。 インデクサー セッターの注釈により、Type[] に格納されているすべての型にコンストラクターが含まれるようになります。 ただし、分析ではこれを確認できず、ゲッターの警告が生成されます。戻り値の型のコンストラクターが保持されていることがわからないためです。

要件が満たされていることが確実な場合は、ゲッターに [UnconditionalSuppressMessage] を追加することで、この警告をサイレント状態にすることができます。

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
            Justification = "The list only contains types stored through the annotated setter.")]
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

リフレクションされたメンバーがリフレクションの可視ターゲットになるようにする注釈またはコードがある場合にのみ警告の非表示は有効であると強調することが重要です。 メンバーが呼び出し、フィールド、プロパティ アクセスのターゲットというだけでは不十分です。 十分な場合もあるかもしれませんが、トリミング最適化が追加されると、そのようなコードは最後に壊れます。 リフレクションの可視ターゲットではないプロパティ、フィールド、メソッドはインライン化されること、その名前が削除されること、異なる型に移動されること、あるいはリフレクションを壊すその他のやり方で最適化されることがあります。 警告を非表示にするとき、他の場所でトリミング アナライザーのリフレクションの可視ターゲットであったターゲットでのみリフレクションが許可されます。

// Invalid justification and suppression: property being non-reflectively
// used by the app doesn't guarantee that the property will be available
// for reflection. Properties that are not visible targets of reflection
// are already optimized away with Native AOT trimming and may be
// optimized away for non-native deployment in the future as well.
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
    Justification = "*INVALID* Only need to serialize properties that are used by"
                    + "the app. *INVALID*")]
public string Serialize(object o)
{
    StringBuilder sb = new StringBuilder();
    foreach (var property in o.GetType().GetProperties())
    {
        AppendProperty(sb, property, o);
    }
    return sb.ToString();
}

DynamicDependency

[DynamicDependency] 属性を使用すると、メンバーが他のメンバーに動的に依存していることを示すことができます。 これにより、参照先のメンバーはこの属性を持つメンバーが保持されている場合は必ず保持されますが、それ自体に対する警告の発生は抑制されません。 コードのリフレクション動作に関するトリミング分析を通知する他の属性とは異なり、[DynamicDependency] では他のメンバーだけを保持します。 これを [UnconditionalSuppressMessage] と共に使用すれば、一部の分析警告を修正することができます。

警告

[DynamicDependency] 属性は、他の方法を実行できない場合に、最後の手段としてのみ使用してください。 [RequiresUnreferencedCode] または [DynamicallyAccessedMembers] を使用して、リフレクション動作を表現することをお勧めします。

[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
    var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
    helper.Invoke(null, null);
}

DynamicDependency がないと、トリミングによって、HelperMyAssembly から削除されたり、MyAssembly が他の場所で参照されていない場合に完全に削除されたりして、実行時に障害が発生する可能性があることを示す警告が表示される場合があります。 この属性を使用することで、Helper が確実に保持されます。

この属性では、string または DynamicallyAccessedMemberTypes を介して保持するメンバーを指定します。 型とアセンブリは、属性コンテキスト内で暗黙的に指定されるか、または属性内で明示的に指定されます (Type によって、または型とアセンブリ名の場合は string によって)。

型とメンバーの文字列では、C# ドキュメントのコメント ID 文字列の形式 のバリエーションを、メンバー プレフィックスなしで使用します。 メンバー文字列には、宣言する型の名前を含めることはできません。指定した名前のすべてのメンバーを保持するにはパラメーターを省略してかまいません。 形式の例を次のコードに示します。

[DynamicDependency("MyMethod()")]
[DynamicDependency("MyMethod(System,Boolean,System.String)")]
[DynamicDependency("MethodOnDifferentType()", typeof(ContainingType))]
[DynamicDependency("MemberName")]
[DynamicDependency("MemberOnUnreferencedAssembly", "ContainingType"
                                                 , "UnreferencedAssembly")]
[DynamicDependency("MemberName", "Namespace.ContainingType.NestedType", "Assembly")]
// generics
[DynamicDependency("GenericMethodName``1")]
[DynamicDependency("GenericMethod``2(``0,``1)")]
[DynamicDependency(
    "MethodWithGenericParameterTypes(System.Collections.Generic.List{System.String})")]
[DynamicDependency("MethodOnGenericType(`0)", "GenericType`1", "UnreferencedAssembly")]
[DynamicDependency("MethodOnGenericType(`0)", typeof(GenericType<>))]

[DynamicDependency] 属性は、DynamicallyAccessedMembersAttribute を使用しても分析できないリフレクション パターンがメソッドに含まれている場合に使用するように設計されています。