System.Runtime.Loader.AssemblyLoadContext について

AssemblyLoadContext クラスは .NET Core に導入されたものであり、.NET Framework では使用できません。 この記事では、AssemblyLoadContext API ドキュメントを概念情報により補完します。

この記事は、動的読み込みを実装する開発者 (特に動的読み込みフレームワークの開発者) に関連しています。

AssemblyLoadContext とは

すべての .NET 5+ および .NET Core アプリケーションでは、暗黙的に AssemblyLoadContext が使用されます。 これは、依存関係を検索して読み込むためのランタイムのプロバイダーです。 依存関係が読み込まれるたびに、AssemblyLoadContext インスタンスが呼び出されてそれを見つけます。

  • AssemblyLoadContext は、マネージド アセンブリやその他の依存関係を検索し、読み込み、キャッシュするサービスを提供します。
  • 動的なコードの読み込みとアンロードをサポートするために、独自の AssemblyLoadContext インスタンスにコードとその依存関係を読み込むための分離コンテキストを作成します。

バージョン管理ルール

1 つの AssemblyLoadContext インスタンスは、単純なアセンブリ名につき 1 つのバージョンの Assembly のみを読み込むように制限されています。 アセンブリ参照が、その名前のアセンブリが既に読み込まれている AssemblyLoadContext インスタンスに対して解決されると、要求されたバージョンは読み込まれたバージョンと比較されます。 読み込まれたバージョンが要求されたバージョン以上である場合にのみ、解決は成功します。

複数の AssemblyLoadContext インスタンスが必要な場合

1 つの AssemblyLoadContext インスタンスは 1 つのバージョンのアセンブリのみを読み込むことができるという制限は、コード モジュールを動的に読み込む場合に問題になるおそれがあります。 各モジュールは個別にコンパイルされ、モジュールは異なるバージョンの Assembly に依存する場合があります。 これは、異なるモジュールが、一般的に使用されるライブラリの異なるバージョンに依存している場合によく問題となります。

動的なコード読み込みをサポートするために、AssemblyLoadContext API を使用して、Assembly の競合するバージョンを同じアプリケーション内に読み込むことができます。 各 AssemblyLoadContext インスタンスは、各 AssemblyName.Name を特定の Assembly インスタンスにマップする一意のディクショナリを提供します。

また、後でアンロードするために、あるコード モジュールに関連する依存関係をグループ化するための便利なメカニズムも提供します。

AssemblyLoadContext.Default インスタンス

AssemblyLoadContext.Default インスタンスは、起動時にランタイムによって自動的に設定されます。 既定のプローブを使用して、すべての静的な依存関係を特定し、検索します。

最も一般的な依存関係読み込みのシナリオを解決します。

動的な依存関係

AssemblyLoadContext には、オーバーライドできるさまざまなイベントと仮想関数があります。

AssemblyLoadContext.Default インスタンスでは、イベントのオーバーライドのみがサポートされます。

マネージド アセンブリの読み込みアルゴリズム」、「サテライト アセンブリの読み込みアルゴリズム」、「アンマネージド (ネイティブ) ライブラリの読み込みアルゴリズム」の各記事では、使用可能なすべてのイベントと仮想関数を参照しています。 これらの記事では、読み込みアルゴリズムでの各イベントと関数の相対位置を示しています。 この記事では、その情報は繰り返しません。

ここでは、関連するイベントおよび関数の一般原則について説明します。

  • 反復可能にする。 特定の依存関係に対するクエリは、常に同じ応答になる必要があります。 同じ読み込み済みの依存関係インスタンスを返す必要があります。 この要件は、キャッシュの整合性の基本となります。 特にマネージド アセンブリについては、Assembly キャッシュが作成されます。 キャッシュ キーは、単純なアセンブリ名 AssemblyName.Name です。
  • 通常はスローしない。 これらの関数は、要求された依存関係を見つけることができない場合、スローするのではなく null を返すことが想定されています。 スローすると、検索が途中で終了し、例外が呼び出し元に伝達されます。 スローは、破損したアセンブリやメモリ不足の状態など、予期しないエラーに限定する必要があります。
  • 再帰を回避する。 これらの関数とハンドラーは、依存関係を検索するための読み込みルールを実装することに注意してください。 実装では、再帰をトリガーする API を呼び出さないでください。 コードでは、通常、特定のパスまたはメモリ参照の引数を必要とする AssemblyLoadContext 読み込み関数を呼び出す必要があります。
  • 正しい AssemblyLoadContext に読み込む。 依存関係を読み込む場所の選択は、アプリケーション固有です。 この選択は、これらのイベントと関数によって実装されます。 コードによって AssemblyLoadContext のパスによる読み込み関数を呼び出すときは、コードを読み込むインスタンス上でそれらを呼び出します。 null を返し、AssemblyLoadContext.Default により読み込みを処理させるのが最も単純なオプションの場合があります。
  • スレッドの競合に注意する。 読み込みは複数のスレッドによってトリガーできます。 AssemblyLoadContext は、キャッシュにアセンブリをアトミックに追加することで、スレッドの競合を処理します。 競合の敗者のインスタンスは破棄されます。 実装ロジックでは、複数のスレッドを適切に処理しない余分なロジックを追加しないでください。

動的な依存関係はどのように分離されるか

AssemblyLoadContext インスタンスは、Assembly インスタンスと Type 定義の一意のスコープを表します。

これらの依存関係の間にバイナリの分離はありません。 名前によって互いを検索しないことによってのみ分離されています。

AssemblyLoadContext は次のようになります。

  • AssemblyName.Name が別の Assembly インスタンスを参照している可能性があります。
  • Type.GetType が、同じ型の name に対して異なる型のインスタンスを返す場合があります。

共有された依存関係

依存関係は、AssemblyLoadContext インスタンス間で簡単に共有できます。 一般的なモデルは、1 つの AssemblyLoadContext が依存関係を読み込むことです。 もう 1 つは、読み込まれたアセンブリへの参照を使用して依存関係を共有します。

この共有は、ランタイム アセンブリに必要です。 これらのアセンブリは、AssemblyLoadContext.Default にのみ読み込むことができます。 ASP.NETWPFWinForms などのフレームワークでも同じことが必要です。

共有の依存関係は AssemblyLoadContext.Default に読み込むことをお勧めします。 この共有は、一般的な設計パターンです。

共有は、カスタム AssemblyLoadContext インスタンスのコーディングで実装されます。 AssemblyLoadContext には、オーバーライドできるさまざまなイベントと仮想関数があります。 これらの関数のいずれかが、別の AssemblyLoadContext インスタンスに読み込まれた Assembly インスタンスへの参照を返すと、Assembly インスタンスが共有されます。 標準読み込みアルゴリズムは、一般的な共有パターンを簡略化するために、読み込みを AssemblyLoadContext.Default に任せます。 詳細については、「マネージド アセンブリの読み込みアルゴリズム」を参照してください。

型変換の問題

2 つの AssemblyLoadContext インスタンスに同じ name の型定義が含まれている場合、それらは同じ型ではありません。 それらが同じ型になるのは、それらが同じ Assembly インスタンスからのものである場合に限ります。

さらに問題が複雑なのは、これらの一致しない型に関する例外メッセージが分かりにくい場合があることです。 型は、例外メッセージの中で単純型の名前によって参照されます。 この場合の一般的な例外メッセージは、次の形式になります。

型 'IsolatedType' のオブジェクトを型 'IsolatedType' に変換できません。

型変換の問題をデバッグする

一致しない型のペアが存在する場合、次の点にも注意する必要があります。

ab の 2 つのオブジェクトがある場合、デバッガーで次のものを評価すると便利です。

// In debugger look at each assembly's instance, Location, and FullName
a.GetType().Assembly
b.GetType().Assembly
// In debugger look at each AssemblyLoadContext's instance and name
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(a.GetType().Assembly)
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(b.GetType().Assembly)

型変換の問題を解決する

このような型変換の問題を解決するには、2 つの設計パターンがあります。

  1. 一般的な共有型を使用します。 この共有型は、プリミティブなランタイム型にすることも、共有アセンブリに新しい共有型を作成することもできます。 多くの場合、共有型はアプリケーション アセンブリで定義された interface です。 詳細については、依存関係の共有方法に関する説明を参照してください。

  2. マーシャリング技法を使用して、ある型から別の型に変換します。