【C#】System.Text.Json における FileLoadException の解決方法

# はじめに
.Net Frameworkの`System.Text.Json`の使用時に、`System.IO.FileLoadException`が発生した際の解決方法を記載する。File**NotFound**Exceptionではないので注意。

## 動作環境
- Windows 10
- Visual Studio 2022
- .Net Framework 4.7.2

## 経緯
下記の手順でフォームコントロールライブラリの**デバッグ**をしたところ、当該エラーが発生した。

1. クラスライブラリプロジェクトに`System.Text.Json.JsonSerializer.Deserialize`を使い、Json文字列をデシリアライズする関数を実装
1. 同ソリューション内のフォームコントロールライブラリプロジェクトから1のクラスを呼び出す処理を実装
1. デバッグ機能で2のコントロールの確認を行う
1. 下記の`FileLoadException`が発生

```Console
例外がスローされました: 'System.IO.FileLoadException' (System.Text.Json.dll の中)
型 'System.IO.FileLoadException' のハンドルされていない例外が System.Text.Json.dll で発生しました
ファイルまたはアセンブリ 'System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'、またはその依存関係の 1 つが読み込めませんでした。見つかったアセンブリのマニフェスト定義はアセンブリ参照に一致しません。 (HRESULT からの例外:0x80131040)
```

# 原因
参照している`System.Runtime.CompilerServices.Unsafe`のバージョンが古い。
.Net Framework 4.7.2 の Unsafe のバージョンは **6.0.0.0** だが、実行時にバージョン**4.0.4.1**を参照しているためアセンブリが見つからない趣旨の例外が発生している。

# 解決方法
アプリケーション系プロジェクトであれば、**バインドリダイレクトの自動生成機能**でバージョンの差異を解決してくれるので、動作確認用のフォームアプリケーションを別途実装すれば良い。ただ、フォームコントロールライブラリからデバッグ機能を使って検証したい場合は`ResolveEventHandler`を実装する必要がある。

※別に解決策ありそうだけど、これしか見つからなかった。

### ResolveEventHandlerを追加する
`System.Runtime.CompilerServices.Unsafe`を参照に追加し、フォームコントロールライブラリ内のどこかに以下の処理を実装する。
```C#
System.AppDomain.CurrentDomain.AssemblyResolve += new System.ResolveEventHandler((object sender, System.ResolveEventArgs args) => {
  var name = new System.Reflection.AssemblyName(args.Name);
  if (name.Name == "System.Runtime.CompilerServices.Unsafe") {
    return typeof(System.Runtime.CompilerServices.Unsafe).Assembly;
  }
  return null;
});
```

# 捕捉
**バインドリダイレクトの自動生成**がアプリケーション系プロジェクトにしか効果ないことを確認するために、以下のプロジェクトを用意して検証してみた。

- クラスライブラリプロジェクト（System.Json.Textを使用し、他2クラスから呼び出される）
- コンソールアプリケーションプロジェクト（クラスライブラリを呼び出して内容を出力）
- フォームコントロールプロジェクト（クラスライブラリを呼び出して内容を出力）

## 用意したソースコード
それぞれのプロジェクトで実装したクラスは下記の通り。

```C#:ClassLibrary1.Class1.cs
namespace ClassLibrary1 {
  public class Class1 {
    public static Person GetPerson() {
      var person = System.Text.Json.JsonSerializer.Deserialize<Person>(@"{""Name"": ""太郎"", ""Age"": 20}");
      return person;
    }
  }

  public class Person {
    public string Name { get; set; }
    public int Age { get; set; }
  }
}
```

```C#:ConsoleApp1.Program.cs
namespace ConsoleApp1 {
  internal class Program {
    static void Main(string[] args) {
      var person = ClassLibrary1.Class1.GetPerson();
      System.Diagnostics.Debug.WriteLine($"{person.Name} {person.Age}");
    }
  }
}

```

```C#:WindowsFormsContorlLibrary1.UserControl.cs
namespace WindowsFormsControlLibrary1 {
  public partial class UserControl1 : System.Windows.Forms.UserControl {
    public UserControl1() {
      InitializeComponent();
      var person = ClassLibrary1.Class1.GetPerson();
      label1.Text = $"{person.Name} {person.Age}";
    }
  }
}
```
更に各プロジェクトを右クリック > プロパティ > アプリケーション > バインドリダイレクトの自動生成にチェックが入っていることを確認する（多分デフォルトでチェック入っている）。

## 実行結果
コンソールアプリケーションをデバッグした場合、デバッグログに正常な出力がされる。

```Console
太郎 20
```

しかし、フォームコントロールライブラリからデバッグした場合、前述の例外発生してしまう。

```Console
例外がスローされました: 'System.IO.FileNotFoundException' (System.Text.Json.dll の中)
型 'System.IO.FileNotFoundException' の例外が System.Text.Json.dll で発生しましたが、ユーザー コード内ではハンドルされませんでした
ファイルまたはアセンブリ 'System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'、またはその依存関係の 1 つが読み込めませんでした。指定されたファイルが見つかりません。
```

## フォームコントロールライブラリで例外が発生する理由
両プロジェクト共に**バインドリダイレクトの自動生成**が有効になっているのに、なぜフォームコントロールライブラリプロジェクトだけうまくいかないのか？
それを知るにはバインドリダイレクトを理解する必要がある。

### バインドリダイレクトとは
[ここ](https://learn.microsoft.com/ja-jp/dotnet/framework/configure-apps/file-schema/runtime/bindingredirect-element)にあるように、バインドリダイレクトは「1つのアセンブリバージョンを別のバージョンにリダイレクトする」機能である。さらに重要なのは、この設定は**アプリケーション構成ファイル（App.config）に定義**する点である。例えばコンソールアプリケーションのバインドリダイレクト自動生成を無効にしリビルド後に実行すると`FileLoadException`が発生し、`App.config`を以下の通り変更して再度リビルドすると正常に動作することが判る。

```xml:ConsoleApp1#App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup> 
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
  </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>
```

### アプリケーション構成ファイルに定義するということは・・・
そう、フォームコントロールライブラリプロジェクトはアセンブリのみの出力＝実行ファイルを持たない＝アプリケーション定義ファイルが存在しないため、バインドリダイレクト機能をプロジェクト単体では使えないのだ。
故に本事象を解決するためには、前述の通り「アプリケーション定義ファイルを持つ別プロジェクトから呼び出す」か「実行時のアセンブリの解決処理に手を加えて無理やり最新バージョンのアセンブリを割り当てて検証」する必要がある。

# さいごに
Windowsデスクトップアプリ開発のためC#に触り始めたけど、根本的な事を理解していないので変なところで躓きまくり・・・。これもデバッグの実行時の引数に構成ファイルを追加するとかそもそも古いバージョンをインストールしてしまうとか別の解決策ありそうだけど、ひとまず解決したので先に進んじゃおう。

# 参考
- [Microsoft Ignite「方法: 自動バインド リダイレクトを有効および無効にする」](https://learn.microsoft.com/ja-jp/dotnet/framework/configure-apps/how-to-enable-and-disable-automatic-binding-redirection)
- [Microsoft Ignite「アセンブリ バージョンのリダイレクト」](https://learn.microsoft.com/ja-jp/dotnet/framework/configure-apps/redirect-assembly-versions#BKMK_Redirectingassemblyversionsattheapplevel)
- [Stack Overflow「Could not load file or assembly System.Runtime.CompilerServices.Unsafe」](https://stackoverflow.com/questions/62764744/could-not-load-file-or-assembly-system-runtime-compilerservices-unsafe)
- [Nick Craver「Binding Redirects」](https://nickcraver.com/blog/2020/02/11/binding-redirects/)
- [teratail「.NETでEXE実行時に「'System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1」「依存関係の 1 つが読み込めませんでした。」」](https://teratail.com/questions/360384)