bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] Application Load Context によるアセンブリ分離を理解する

[C#] Application Load Context によるアセンブリ分離を理解する

Jun 25, 2026 C# , .NET bucket-sort

前回は、.NET のアプリケーションドメイン(AppDomain)について見ました。

.NET Framework では、アセンブリの分離やアンロードを考えるときに AppDomain が重要でした。
しかし、現在の .NET(.NET Core / .NET 5 以降)では、アセンブリの読み込み、分離、アンロードには Application Load Context、つまり AssemblyLoadContext を使うのが基本です。

今回は「Application Load Context によるアセンブリ分離」をテーマに、AssemblyLoadContext が何を解決するのかを整理します。

アセンブリ分離とは何か

.NET アプリケーションは、実行時に複数のアセンブリを読み込みます。
アセンブリとは、.dll や .exe として配置される .NET の実行単位です。

通常のアプリケーションでは、必要なアセンブリは既定の読み込み領域に読み込まれます。
しかし、プラグイン機構や拡張機能を作る場合、次のような問題が出てきます。

  • プラグインごとに異なるバージョンの依存アセンブリを使いたい
  • アプリ本体とプラグインの依存関係を分離したい
  • 不要になったプラグインをアンロードしたい
  • 実行時に外部 DLL を読み込んで処理したい

このような場面で使うのが AssemblyLoadContext です。

AssemblyLoadContext は、アセンブリを読み込むための「文脈」または「読み込み領域」です。
読み込み領域を分けることで、同じ名前でも異なるバージョンのアセンブリを扱ったり、プラグインごとに依存関係を分けたりできます。

AssemblyLoadContext の役割

AssemblyLoadContext は System.Runtime.Loader 名前空間にあるクラスです。

using System.Runtime.Loader;

主な役割は次の 3 つです。

  • アセンブリを読み込む
  • アセンブリの依存関係を解決する
  • 必要に応じて読み込んだアセンブリをアンロードする

アプリケーションを起動すると、まず既定の AssemblyLoadContext が作られます。
通常の参照アセンブリは、この既定コンテキストに読み込まれます。

using System;
using System.Runtime.Loader;

AssemblyLoadContext context = AssemblyLoadContext.Default;

Console.WriteLine(context.Name);
Console.WriteLine(context.IsCollectible);

AssemblyLoadContext.Default は、アプリケーションの標準的な読み込み先です。
IsCollectible は、そのコンテキストをアンロードできるかどうかを示します。既定コンテキストはアンロードできません。

既定の Application Load Context

既定の AssemblyLoadContext は、アプリケーション本体が通常使う読み込み領域です。

次のコードでは、現在読み込まれているアセンブリがどの AssemblyLoadContext に属しているかを確認しています。

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;

Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();

foreach (Assembly assembly in assemblies.OrderBy(a => a.GetName().Name))
{
    AssemblyLoadContext? context = AssemblyLoadContext.GetLoadContext(assembly);

    Console.WriteLine($"Assembly: {assembly.GetName().Name}");
    Console.WriteLine($"Context: {context?.Name}");
    Console.WriteLine($"Collectible: {context?.IsCollectible}");
    Console.WriteLine();
}

多くのアセンブリは、既定のコンテキストに読み込まれているはずです。

カスタム AssemblyLoadContext を作る

プラグインのように、本体とは別の場所からアセンブリを読み込みたい場合は、独自の AssemblyLoadContext を作ります。

using System.Reflection;
using System.Runtime.Loader;

public sealed class PluginLoadContext : AssemblyLoadContext
{
    private readonly AssemblyDependencyResolver resolver;

    public PluginLoadContext(string pluginPath)
        : base(isCollectible: true)
    {
        resolver = new AssemblyDependencyResolver(pluginPath);
    }

    protected override Assembly? Load(AssemblyName assemblyName)
    {
        string? assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);

        if (assemblyPath == null)
        {
            return null;
        }

        return LoadFromAssemblyPath(assemblyPath);
    }
}

この例では、AssemblyDependencyResolver を使って、プラグイン側の依存アセンブリを解決しています。

Load() が null を返した場合、ランタイムは別の解決方法を試します。
つまり、独自コンテキストで解決できるものは読み込み、解決できないものは通常の仕組みに委ねる、という流れになります。

プラグインアセンブリを読み込む

次のコードは、カスタム AssemblyLoadContext を使ってプラグイン DLL を読み込む例です。

using System;
using System.Reflection;

string pluginPath = @"C:\Plugins\SamplePlugin\SamplePlugin.dll";

var loadContext = new PluginLoadContext(pluginPath);
Assembly pluginAssembly = loadContext.LoadFromAssemblyPath(pluginPath);

Console.WriteLine($"読み込み完了: {pluginAssembly.FullName}");

Type? pluginType = pluginAssembly.GetTypes()
    .FirstOrDefault(type => type.Name == "SamplePlugin");

if (pluginType == null)
{
    Console.WriteLine("対象の型が見つかりませんでした。");
    return;
}

object? instance = Activator.CreateInstance(pluginType);
MethodInfo? method = pluginType.GetMethod("Run");

method?.Invoke(instance, null);

この例では、SamplePlugin 型を探して、Run メソッドを呼び出しています。

実務では、リフレクションで毎回メソッド名を探すよりも、プラグイン用の共通インターフェイスを別アセンブリに定義し、本体とプラグインの両方から参照する設計が扱いやすいです。

collectible なコンテキストとアンロード

AssemblyLoadContext の大きな特徴は、isCollectible: true で作成するとアンロード可能になることです。

var loadContext = new PluginLoadContext(pluginPath);
Assembly assembly = loadContext.LoadFromAssemblyPath(pluginPath);

// プラグイン処理を実行する

loadContext.Unload();

ただし、Unload() を呼んだからといって、即座にメモリから消えるとは限りません。
アンロードは GC と連動します。読み込んだアセンブリ、型、インスタンス、デリゲートなどへの参照が残っていると、コンテキストは解放されません。

アンロード確認をしたい場合は、WeakReference を使って参照が解放されたかを確認します。

using System;
using System.Reflection;

static WeakReference LoadAndUnload(string pluginPath)
{
    var loadContext = new PluginLoadContext(pluginPath);
    Assembly assembly = loadContext.LoadFromAssemblyPath(pluginPath);

    Console.WriteLine($"Loaded: {assembly.FullName}");

    var weakReference = new WeakReference(loadContext, trackResurrection: false);

    loadContext.Unload();

    return weakReference;
}

WeakReference reference = LoadAndUnload(@"C:\Plugins\SamplePlugin\SamplePlugin.dll");

for (int i = 0; reference.IsAlive && i < 10; i++)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Console.WriteLine(reference.IsAlive
    ? "まだアンロードされていません。"
    : "アンロードされました。");

アンロードされない場合は、どこかに参照が残っています。
特に、静的フィールド、イベント購読、キャッシュ、長寿命のデリゲートには注意が必要です。

AppDomain との違い

AppDomain と AssemblyLoadContext は、どちらも「分離」に関係します。
ただし、分離する対象と時代背景が異なります。

  • AppDomain: .NET Framework 時代の実行境界。コード実行の分離、セキュリティ境界、アセンブリのまとめてアンロードに使われた
  • AssemblyLoadContext: 現在の .NET におけるアセンブリ読み込みの分離単位。プラグイン、依存関係の分離、アンロードに使う

現在の .NET で「プラグインごとに DLL を分けたい」「読み込んだ DLL を後で外したい」と考えるなら、まず AssemblyLoadContext を検討します。

使いどころ

AssemblyLoadContext が役立つのは、次のような場面です。

  • プラグインアーキテクチャを作る
  • 外部 DLL を実行時に読み込む
  • プラグインごとに依存アセンブリのバージョンを分ける
  • 使い終わったプラグインをアンロードする
  • テストやツールでアセンブリ読み込み状態を分離する

一方で、通常のアプリケーションで参照アセンブリを読み込むだけなら、明示的に AssemblyLoadContext を作る必要はありません。
標準の参照解決で十分な場合は、既定コンテキストに任せる方がシンプルです。

まとめ

AssemblyLoadContext は、現代の .NET におけるアセンブリ分離の中心です。
通常のアプリケーションは既定のコンテキストで動きますが、プラグインや動的ロードでは、独自のコンテキストを作ることで依存関係を分離できます。

特に isCollectible: true のコンテキストは、読み込んだアセンブリを後でアンロードしたい場合に重要です。
ただし、アンロードには参照の解放が必要なので、設計段階から「読み込む」「使う」「参照を切る」「アンロードする」という流れを意識しておく必要があります。

C# .NET AssemblyLoadContext Assembly Plugin Reflection CLR
← [C#] .NET アプリケーションドメインを理解する [C#] マルチスレッド処理の基礎を理解する →

Related Posts

  • [C#] .NET アプリケーションドメインを理解する Jun 24, 2026
  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026
  • [C#] 外部アセンブリを読み込み実行時に型を呼び出す Jul 7, 2026
  • [C#] 型メタデータとリフレクションの基礎を理解する Jul 5, 2026

Table of Contents

  • アセンブリ分離とは何か
  • AssemblyLoadContext の役割
  • 既定の Application Load Context
  • カスタム AssemblyLoadContext を作る
  • プラグインアセンブリを読み込む
  • collectible なコンテキストとアンロード
  • AppDomain との違い
  • 使いどころ
  • まとめ

Recent Posts

  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026
  • [C#] 属性でコードに意味を与える Jul 8, 2026
  • [C#] 外部アセンブリを読み込み実行時に型を呼び出す Jul 7, 2026
  • [C#] リフレクションで型の中身を調べる Jul 6, 2026
  • [C#] 型メタデータとリフレクションの基礎を理解する Jul 5, 2026

Categories

  • C#120
  • .NET119
  • AWS27
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Diagnostics1
  • Filament1
  • Git1
  • SQS1

Tags

  • C#
  • .NET
  • AWS
  • Laravel
  • コレクション
  • PHP
  • セキュリティ
  • MySQL
  • Linux
  • パフォーマンス
  • Apache
  • System.Collections.Generic
  • デリゲート
  • Code Snippet
  • DynamoDB
  • LINQ
  • NoSQL
  • PHP-FPM
  • RDS
  • System.Collections
  • Windows
  • メモリ管理
  • リフレクション
  • DoS
  • Nginx
  • WordPress
  • メタデータ
  • ラムダ式
  • 監視
  • 設計
  • Amazon Linux 2023
  • Delegate
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • LINQ to Objects
  • OPCache
  • Pointer
  • Reflection
  • System.Collections.Specialized
  • Unsafe
  • Webサーバー
  • インターフェース
  • オブジェクト指向
  • クラス設計
  • デザインパターン
  • パターンマッチング
  • ポインター
  • 継承
Powered by Hugo & Explore Theme.