Blazor - レンダーモードが混在する場合、DotNet.InvokeMethodAsync (JavaScript からの .NET 静的メソッド呼び出し) は使えない

## Blazor の JavaScript 相互運用機能

Blazor は C# (.NET) でフルスタック Web アプリケーションを実装可能とする、コンポーネント指向のフレームワークです。しかしながら、すべてのブラウザ API が C# 側のインターフェースとして用意されているわけではありません。そのため、Blazor には、C# (.NET) 側と JavaScript 側とで相互に呼び出しを行える、JavaScript 相互運用機能が用意されています。Blazor の JavaScript 相互運用には様々なパターンがあります。

- JavaScript 側からの、C# (.NET) 側の型の静的メソッド呼び出し
- JavaScript 側からの、 C# (.NET) 側の値のインスタンスメソッド呼び出し
- C# (.NET) 側からの、JavaScript 側のグローバル名前空間にあるオブジェクトのメソッド呼び出し
- C# (.NET) 側からの、JavaScript モジュールのインポート
- C# (.NET) 側からの、JavaScript モジュールでエクスポートされたメンバの呼び出し
- etc.

今回はその中でも、JavaScript 側から、C# (.NET) 側の static メソッドを呼び出す場合の問題について共有します。

## JavaScript 側から C# (.NET) 側の static メソッドを呼び出す

Blazor アプリケーションにおいて、JavaScript 側から、C# (.NET) 側の static メソッドを呼び出すには、まずは C# (.NET) 側にて、JavaScript 側から呼び出される static メソッドを実装し、`[JSInvokable]` 属性を付けておきます。例えば以下のとおりです。

```cs:Foo.cs
namespace BlazorApp1;

public class Foo {
 
    [JSInvokable]
    public static void Bar() {
        // ここで何かする
    }
}
```

その上で、JavaScript 側からは以下のように、`DotNet` オブジェクトの `invokeMethodAsync` メソッドを呼び出すことで、C# (.NET) 側の対象 static メソッドが実行されます。

```js
DotNet.invokeMethodAsync('BlazorApp1', 'Bar');
```

`invokeMethodAsync` メソッドの第1引数は呼び出し対象の static メソッドを含むアセンブリの名前、第2引数は `[JSInvokable]` 属性を付けたメソッドの名前になります。今回の例では、第3引数以降は指定していませんが、もし指定した場合は、それら引数は、第1引数・第2引数で識別される C#  (.NET) 側 static メソッドを実行する際の引数として渡されます。

## 複数のレンダーモードをまたがって遷移すると、呼び出しが例外になる

しかし、この JavaScript 側からの C# (.NET) 側 static メソッド呼び出しは、.NET 8 で登場した複数のレンダーモードを混在した Blazor アプリケーションでは強い制約があり、自分としては実質使用不可、と考えています。

その制約とは、「当該 Blazor アプリケーションを読み込んだ以降、**いちどでも異なるレンダーモードをまたがる**と、JavaScript 側の **`DotNet.invokeMethodAsync` 呼び出しが例外になる**」というものです。例外メッセージは以下のとおりです。

```:ブラウザの開発者コンソール出力
Uncaught Error: There are multiple .NET runtimes present,
so a default dispatcher could not be resolved. 
Use DotNetObject to invoke .NET instance methods.
    at v (blazor.web.js:1:1693)
    at e.invokeMethodAsync (blazor.web.js:1:2084)
    ...
```

例えば、以下のように、"Home" ページはサーバー側静的レンダリングモード、"Counter" ページは対話 Server モードである Blazor Web アプリケーションで、前述のような C# (.NET) 側 static メソッド呼び出しの実装をしていたとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/bf0b0f33-c44e-c188-26c1-9ecbfb1c2b00.png)

このアプリで、まずいったん "Counter" ページを訪問します。なお、この時点では `DotNet.invokeMethodAsync` 呼び出しは正常動作します。
次に "Home" ページに移動して、いったんサーバーとの SignalR 接続が閉じられるのを待ちます。
その後に再び "Counter" ページを訪問すると、それ以降、`DotNet.invokeMethodAsync` 呼び出しを実行すると「There are multiple .NET runtimes present...」例外が発生してしまうのです。

他にも、対話 Server モードのページと対話 WebAssembly モードのページを行き交っても同様で、同じく例外発生する現象となります。いずれの対話モードのページも、初回ロード時は大丈夫なのですが、前述のとおり、異なる対話レンダーモードのページ間を行き来して以降、前述の例外が発生してしまいます。

このような制約があるため、**レンダーモードが混在する Blazor アプリケーション上では、JavaScript からの .NET 静的メソッド呼び出しは、事実上、使い物にならない**と考えています。

## どうして例外発生するのか

この制約について、自分が知る限り、回避策・緩和策はありません。そもそもレンダーモードが混在した環境では、例外メッセージが丁寧に説明してくれているとおり、ブラウザ内の WebAssembly 上で実行中の .NET ランタイムと、SignalR 接続で結ばれたサーバー側で実行中の .NET ランタイムの、**2 つの .NET ランタイムが同居**する状況が発生し得ます。そのような状況で、C# (.NET) の static メソッドを呼び出そうにも、**WebAssembly 側かサーバー側か、どちらの static メソッドを呼ぶべきか、区別がつきません。** そのような事情から、このような制約・振る舞いとなっているのだと、自分は理解しています。

なお、対話 Server モードと静的レンダリングモードを行き来しただけの場合は、一見、サーバー側 .NET ランタイムしかないので問題なく動作しても良さそうにも思われますが、いったん SignalR 接続が閉じられて再接続した場合は、同じサーバープロセスに再接続するとも限らず、そうなると動作が一貫しなくなるので、やはり、"複数の .NET ランタイム" として扱われるのかもしれません。

## レンダーモードの混在をやめるか、.NET のインスタンスメソッドの呼び出しに修正を

複数のレンダーモードの必要がなければ、.NET 8 より前のバージョンから用意されていたシンプルな Blazor Server あるいは Blazor WebAssembly アプリケーションとしてホスティングモデルを修正するのがよいでしょう。あるいは複数のレンダーモードが必要ということであれば、実直に、JavaScript 側からの C# (.NET) 側の呼び出しは、static メソッド呼び出しではなく .NET オブジェクトに対するインスタンスメソッド呼び出しとして実現できるよう、アプリケーションの構造を作り直すしかなさそうです。

以上、Blazor アプリケーションで `DotNet.InvokeMethodAsync` を使用されている場合は、ご注意ください。