C#を用いたアプリケーション開発において、開発者を最も悩ませる例外の一つが「System.IO.FileNotFoundException: Could not load file or assembly」です。

ビルドは正常に終了したにもかかわらず、実行時に突然発生するこのエラーは、原因の特定に時間を要することが少なくありません。

多くの場合、このエラーは単にファイルが存在しないだけでなく、ライブラリの依存関係やバージョンの不一致、あるいは実行環境の構成ミスによって引き起こされます。

本記事では、この例外が発生するメカニズムを深く掘り下げ、実務で役立つ具体的な解決策を体系的に解説します。

System.IO.FileNotFoundException(アセンブリ読み込みエラー)の本質

まず、この例外が通常の「ファイルが見つからない」という状況とどのように異なるのかを理解することが重要です。

通常のFileNotFoundExceptionは、プログラムがテキストファイルや画像ファイルを開こうとした際に、指定されたパスにファイルが存在しない場合に発生します。

しかし、メッセージに「Could not load file or assembly」と含まれている場合は、.NETランタイムがプログラムの実行に必要なDLL(アセンブリ)をロードできなかったことを意味します。

.NETアプリケーションは、実行中に必要に応じて外部のライブラリを動的に読み込みます。

このプロセスは「アセンブリの解決」と呼ばれ、ランタイムは特定の探索ルールに基づいて対象のファイルを探し出します。

この探索プロセスが失敗したときに、今回取り上げる例外がスローされます。

つまり、物理的にファイルが存在していたとしても、ランタイムが要求する条件(バージョン、公開キー、アーキテクチャなど)と一致しない場合、このエラーが発生するのです。

エラーが発生する主な5つの原因

このエラーを解消するためには、まず「なぜランタイムがアセンブリを見つけられなかったのか」という理由を切り分ける必要があります。

1. 実行ディレクトリにDLLが存在しない

最も単純かつ頻繁に起こる原因は、実行ファイル(.exe)と同じフォルダに必要なDLLが配置されていないことです。

プロジェクトの参照設定で「ローカルにコピー」が「False」になっている場合、ビルド出力フォルダにライブラリがコピーされません。

また、CI/CDパイプラインでのデプロイ漏れや、インストーラーの設定ミスによってもこの状況が発生します。

2. アセンブリのバージョン不一致(強固な名前の競合)

アプリケーションが参照しているライブラリAが、ライブラリBのバージョン1.0を要求しているとします。

しかし、実行環境にはライブラリBのバージョン2.0しか存在しない場合、ランタイムはロードを拒否することがあります。

特に「厳密な名前(Strong Name)」が付与されているアセンブリでは、バージョンが1つでも異なると別のアセンブリとして扱われるため、このエラーが発生しやすくなります。

3. プロセッサアーキテクチャの不整合(x86 vs x64)

64ビット(x64)で動作しているプロセスが、32ビット(x86)専用にビルドされたDLLを読み込もうとした場合に発生します。

逆に、32ビットプロセスが64ビット専用DLLを読み込むこともできません。

この場合、ファイル自体は見えているものの、「形式が正しくない」あるいは「読み込みに失敗した」という結果になり、FileNotFoundExceptionとして報告されることがあります。

4. 依存先の依存先(間接的な依存関係)が不足している

自分のプロジェクトが直接参照しているライブラリ(ライブラリA)は存在していても、そのライブラリAが内部で利用している別のライブラリ(ライブラリB)が欠落しているパターンです。

NuGetパッケージを利用している場合、依存関係の解決が不完全だと、このような連鎖的な欠落が発生します。

5. ターゲットフレームワークの乖離

.NET Framework向けに作成された古いライブラリを、最新の.NET(.NET 6/7/8以降)から読み込もうとした際に、互換性の問題でロードに失敗することがあります。

また、その逆で、新しいランタイムを必要とするアセンブリを古い環境で実行しようとした場合も同様です。

トラブルシューティングの実践的な手順

エラーが発生した際、闇雲に設定を変更するのではなく、以下の手順で原因を特定しましょう。

InnerExceptionと詳細メッセージの確認

例外が発生した際、デバッガで「Exceptionの表示」を確認し、InnerExceptionに真の原因が隠れていないか調べます。

詳細メッセージには、「どのファイルを探そうとしたか」や「どのバージョンを要求したか」が具体的に記載されています。

以下のコードのように、try-catchブロックで例外を捕捉し、ログを出力するのが基本です。

C#
try
{
    // アセンブリのロードが発生する処理
    RunApplication();
}
catch (System.IO.FileNotFoundException ex)
{
    // エラーメッセージと、ロードに失敗したファイル名を出力する
    Console.WriteLine($"Message: {ex.Message}");
    Console.WriteLine($"FileName: {ex.FileName}");
    Console.WriteLine($"FusionLog: {ex.FusionLog}"); // .NET Frameworkの場合に有効
}
実行結果
Message: Could not load file or assembly 'Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' or one of its dependencies. 指定されたファイルが見つかりません。
FileName: Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed

Fusion Log Viewer (Fuslogvw.exe) の活用

.NET Frameworkを使用している場合、ランタイムがアセンブリを探索したログを記録する「Fusion Log Viewer」が非常に強力です。

このツールを使用すると、「どのフォルダを探し、なぜ失敗したのか」という試行プロセスをすべて可視化できます。

管理権限でツールを起動し、設定で「ディスクへのログ記録」を有効にすることで、詳細な解析が可能になります。

Dependency Walker または dependencies.exe の利用

DLL間の依存関係を視覚化するツールを使用することで、どの二次的なDLLが不足しているかを即座に特定できます。

最近では、オープンソースで公開されている「Dependencies」というツールが、モダンなWindows環境に適しており推奨されます。

具体的な解決策

原因が特定できたら、以下の解決策を順に試してください。

1. 「ローカルにコピー」をTrueに設定する

Visual Studioのソリューションエクスプローラーで、参照しているライブラリを選択します。

プロパティウィンドウを開き、「ローカルにコピー(Copy Local)」が「True」になっていることを確認してください。

これが「False」の場合、ビルド時に出力ディレクトリへDLLがコピーされないため、実行時にエラーとなります。

2. NuGetパッケージの復元とクリーンビルド

依存関係が混乱している場合、一度すべてのキャッシュをクリアするのが近道です。

ソリューションを右クリックして「ソリューションのクリーン」を実行した後、binフォルダとobjフォルダを手動で削除します。

その後、「NuGetパッケージの復元」を行い、再度ビルドを実行してください。

3. アセンブリバインディングのリダイレクト設定

複数のライブラリが異なるバージョンの同じDLLを要求している場合、app.configまたはweb.configにバインドリダイレクトを記述します。

これにより、古いバージョンへの要求を強制的に新しいバージョンへ転送することができます。

XML
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
        <!-- バージョン 0.0.0.0 から 13.0.0.0 への要求を 13.0.0.0 に向ける -->
        <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

なお、.NET 5以降のモダンな.NETでは、この設定は自動的に生成される .deps.json ファイルによって管理されるため、通常手動で編集する必要はありません。

4. プラットフォームターゲットの統一

プロジェクトのプロパティから「ビルド」タブを選択し、「プラットフォームターゲット」を確認します。

「Any CPU」が推奨されますが、特定のネイティブDLLを利用している場合は「x64」または「x86」に統一する必要があります。

プロジェクト全体でこの設定が不一致だと、アセンブリの読み込みに失敗します。

5. GAC (Global Assembly Cache) の確認

.NET Frameworkの場合、共通のライブラリがGACに登録されていることを期待して動作する場合があります。

開発環境では動作するが本番環境で動作しないという場合、本番環境のGACに必要なライブラリがインストールされていない可能性があります。

ただし、現代の開発手法では、可能な限りGACに頼らず、アプリケーションと同じフォルダにすべてのDLLを同梱する(ポータブルな構成にする)ことが推奨されます。

高度な解決策:AssemblyResolveイベントの実装

どうしても標準の探索ルールで解決できない特殊なケース(例:DLLをサブフォルダに隠蔽したい場合など)は、プログラムコードで解決を試みることができます。

AppDomain.CurrentDomain.AssemblyResolveイベントをハンドルすることで、ランタイムがアセンブリを見つけられなかった際の「最後の手段」を提供できます。

C#
using System;
using System.IO;
using System.Reflection;

class Program
{
    static void Main()
    {
        // アセンブリの解決に失敗したときのカスタムロジックを登録
        AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
        {
            // 探しているアセンブリの名前を取得
            string assemblyName = new AssemblyName(args.Name).Name;
            string customPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "libs", assemblyName + ".dll");

            if (File.Exists(customPath))
            {
                // 指定したパスから手動でロードして返す
                return Assembly.LoadFrom(customPath);
            }
            return null;
        };

        // 実際の処理を開始
        StartProcess();
    }

    static void StartProcess()
    {
        // ここで依存DLLが必要になる
    }
}

この手法は強力ですが、デバッグを困難にする可能性があるため、最終手段として検討してください。

まとめ

System.IO.FileNotFoundException: Could not load file or assemblyは、一見すると単純なエラーですが、その背景には.NETの複雑なアセンブリ解決メカニズムが関わっています。

解決の鍵は、エラーメッセージを注意深く読み、要求されている「正確なバージョン」と「ファイル名」を特定することにあります。

多くの場合、出力フォルダの確認、NuGetの整理、あるいはバインドリダイレクトの設定で解決可能です。

また、昨今の.NET開発では、依存関係を自己完結型(Self-contained)でパブリッシュすることで、これらの問題を未然に防ぐ手法も一般的になっています。

エラーが発生した際は落ち着いて、本記事で紹介したトラブルシューティングの手順を一つずつ試してみてください。

適切な知識を持って対処すれば、必ず解決できるエラーです。