次の方法で共有


ASP.NET Core BlazorSignalR ガイダンス

注意

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 9 バージョンを参照してください。

この記事では、Blazor アプリで SignalR 接続を構成および管理する方法について説明します。

ASP.NET Core SignalR の構成に関する一般的なガイダンスについては、ドキュメントの ASP.NET Core SignalR の概要に関するトピック (特に、ASP.NET Core SignalR の構成) をご覧ください。

サーバー側のアプリでは、ブラウザーとの通信に ASP.NET Core SignalR が使用されます。 SignalR のホストとスケーリングの条件は、サーバー側のアプリに適用されます。

Blazor は、待ち時間、信頼性、およびセキュリティが低いために WebSocket を SignalR トランスポートとして使用する場合に最適です。 WebSocket が使用できない場合や、ロング ポーリングを使用するようにアプリが明示的に構成されている場合は、SignalR によってロング ポーリングが使用されます。

ステートフル再接続を使用した Azure SignalR サービス

ステートフル再接続 (WithStatefulReconnect) は .NET 8 でリリースされましたが、Azure SignalR サービスでは現在サポートされていません。 詳細については、「ステートフル再接続のサポート (Azure/azure-signalr #1878)」を参照してください。

対話型サーバー コンポーネントの WebSocket 圧縮

既定では、対話型サーバー コンポーネントは以下を実行します。

  • WebSocket 接続の圧縮を有効にします。 DisableWebSocketCompression (既定値: false)は WebSocket の圧縮を制御します。

  • 'self' に設定された frame-ancestors コンテンツ セキュリティ ポリシー (CSP) ディレクティブを採用します。このディレクティブは、圧縮が有効になっているか、WebSocket コンテキストの構成が提供される場合にだけ、アプリをそのアプリの提供元の <iframe> に埋め込むことを許可します。 ContentSecurityFrameAncestorPolicyframe-ancestors CSP を制御します。

CSP を一元的な方法で構成する必要がある場合、frame-ancestors CSP は、ContentSecurityFrameAncestorsPolicy の値を null に設定することで手動で削除できます。 frame-ancestors CSP を一元的に管理する場合は、最初のドキュメントがレンダリングされるたびにポリシーを適用するように注意する必要があります。 アプリが攻撃に対して脆弱になる可能性があるため、ポリシーを完全に削除することはお勧めしません。

ConfigureWebSocketAcceptContextを使用して、サーバー コンポーネントによって使用される Websocket 接続のWebSocketAcceptContextを構成します。 既定では、圧縮を有効にし、 ContentSecurityFrameAncestorsPolicy で定義されているフレーム先祖の CSP を設定するポリシーが適用されます。

使用例:

DisableWebSocketCompressiontrue に設定することで圧縮を無効にします。これにより、アプリの攻撃に対する脆弱性は減少しますが、パフォーマンスが低下する可能性があります。

builder.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(o => o.DisableWebSocketCompression = true)

圧縮が有効になっている場合は、'none' という値 (単一引用符が必要) を使用してより厳密な frame-ancestors CSP を構成します。これにより、WebSocket 圧縮は実行できますが、ブラウザーはアプリを <iframe> に埋め込むことができなくなります。

builder.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")

圧縮が有効になっている場合は、ContentSecurityFrameAncestorsPolicynull に設定することで frame-ancestors CSP を削除します。 このシナリオが推奨されるのは、一元化された方法で CSP を設定するアプリに対してだけです。

builder.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = null)

重要

ブラウザーは、最も厳密なポリシー ディレクティブ値を使用して、複数の CSP ヘッダーの CSP ディレクティブを適用します。 したがって、開発者は意図的または誤って 'self' より弱い frame-ancestors ポリシーを追加することができません。

ContentSecurityFrameAncestorsPolicy に渡される文字列値には、単一引用符が必要です。

サポートされていない値: noneself

サポートされている値: 'none''self'

追加のオプションには、1 つ以上のホスト ソースとスキーム ソースの指定が含まれます。

セキュリティへの影響については、「ASP.NET Core Blazor 対話型サーバー側レンダリングに関する脅威の軽減策についてのガイダンス」を参照してください。 frame-ancestors ディレクティブの詳細については、「CSP: frame-ancestors (MDN ドキュメント)」を参照してください。

ホット リロードの応答圧縮を無効にする

ホット リロードを使用する場合は、Development 環境の応答圧縮ミドルウェアを無効にします。 プロジェクト テンプレートからの既定のコードを使用するかどうかに関係なく、要求処理パイプラインで常に最初に UseResponseCompression を呼び出します。

Program ファイルで次のように指定します。

if (!app.Environment.IsDevelopment())
{
    app.UseResponseCompression();
}

クライアント側の認証用 SignalR クロスオリジン ネゴシエーション

このセクションでは、Cookie や HTTP 認証ヘッダーなどの資格情報を送信するように SignalR の基となるクライアントを構成する方法について説明します。

SetBrowserRequestCredentials を使用して、クロスオリジン fetch 要求に Include を設定します。

IncludeRequestCredentialsMessageHandler.cs:

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Http;

public class IncludeRequestCredentialsMessageHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        return base.SendAsync(request, cancellationToken);
    }
}

ハブ接続が構築されている場合は、HttpMessageHandlerHttpMessageHandlerFactory オプションに割り当てます。

private HubConnectionBuilder? hubConnection;

...

hubConnection = new HubConnectionBuilder()
    .WithUrl(new Uri(Navigation.ToAbsoluteUri("/chathub")), options =>
    {
        options.HttpMessageHandlerFactory = innerHandler => 
            new IncludeRequestCredentialsMessageHandler { InnerHandler = innerHandler };
    }).Build();

前の例では、ハブ接続 URL を /chathub の絶対 URI アドレスに構成しています。 URI は、文字列 (https://signalr.example.com など) または構成を使用して設定することもできます。 Navigation は挿入された NavigationManager です。

詳しくは、「ASP.NET Core SignalR の構成」をご覧ください。

クライアント側レンダリング

プリレンダリングが構成されている場合、サーバーへのクライアント接続が確立される前に、プリレンダリングが行われます。 詳細については、「ASP.NET Core Razor のプリレンダリングとコンポーネント」をご覧ください。

プリレンダリングが構成されている場合、サーバーへのクライアント接続が確立される前に、プリレンダリングが行われます。 詳細については、次の記事を参照してください。

プリレンダリングされた状態サイズと SignalR メッセージ サイズの制限

プリレンダリングされた状態サイズが大きい場合、 Blazorの SignalR 回線メッセージ サイズの制限を超える可能性があり、その結果、次のようになります。

  • この SignalR 回線は、クライアントで次のエラーで初期化に失敗します: Circuit host not initialized.
  • 回線が失敗状態になると、クライアント側に再接続 UI が表示されます。 復旧はできません。

この問題を解決するには、次の "いずれかの" 方法を使用します。

  • プリレンダリングされた状態に入れるデータの量を減らします。
  • SignalR メッセージ サイズの制限を増やします。 警告: 上限を引き上げると、サービス拒否 (DoS) 攻撃のリスクが高まる可能性があります。

その他のクライアント側リソース

サーバー側の webfarm ホスティングにセッション アフィニティ (スティッキー セッション) を使用する

複数のバックエンド サーバーが使用されている場合、アプリでは、スティッキー セッションとも呼ばれる、セッション アフィニティを実装する必要があります。 セッション アフィニティにより、接続が切断された場合にクライアントの回線が同じサーバーに再接続されることが保証されます。これは重要です。これは、クライアントの状態が、最初にクライアントの回線を確立したサーバーのメモリにのみ保持されるためです。

セッション アフィニティが webfarm で有効になっていないアプリからは、次のエラーがスローされます。

Uncaught (in promise) Error: Invocation canceled due to the underlying connection being closed.

Azure App Service ホスティングとのセッション アフィニティの詳細については、「ASP.NET Core サーバー側の Blazor アプリをホストしてデプロイする」をご覧ください。

Azure SignalR Service

オプションの Azure SignalR Service はアプリの SignalR ハブと連携して、サーバー側のアプリを多数の同時接続にスケールアップします。 さらに、 サービスのグローバル リーチとハイパフォーマンスのデータ センターは、地理的条件による待機時間の短縮に役立ちます。

このサービスは、Azure App Service または Azure Container Apps でホストされている Blazor アプリには必要ありませんが、他のホスティング環境で役立つ場合があります。

  • 接続のスケールアウトを容易にするには。
  • グローバル分散を実行します。

詳細については、ASP.NET Core サーバー側 Blazor アプリのホストとデプロイに関する記事を参照してください。

サーバー側の回線ハンドラーのオプション

CircuitOptions を使用して回線を構成します。 参照ソースの既定値を表示します。

Note

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

オプションの AddInteractiveServerComponents へのデリゲートを使用して、Program ファイルのオプションを読み込みまたは設定します。 {OPTION} プレースホルダーがオプションを表し、{VALUE} プレースホルダーが値です。

Program ファイルで次のように指定します。

builder.Services.AddRazorComponents().AddInteractiveServerComponents(options =>
{
    options.{OPTION} = {VALUE};
});

オプションの AddServerSideBlazor へのデリゲートを使用して、Program ファイルのオプションを読み込みまたは設定します。 {OPTION} プレースホルダーがオプションを表し、{VALUE} プレースホルダーが値です。

Program ファイルで次のように指定します。

builder.Services.AddServerSideBlazor(options =>
{
    options.{OPTION} = {VALUE};
});

オプションの AddServerSideBlazor へのデリゲートを使用して、Startup.ConfigureServices のオプションを読み込みまたは設定します。 {OPTION} プレースホルダーがオプションを表し、{VALUE} プレースホルダーが値です。

Startup.csStartup.ConfigureServices で:

services.AddServerSideBlazor(options =>
{
    options.{OPTION} = {VALUE};
});

HubConnectionContext を構成するには、HubConnectionContextOptions と共に AddHubOptions を使用します。 参照ソースのハブ接続コンテキスト オプションの既定値を表示します。 SignalR ドキュメントのオプションの説明については、「ASP.NET Core SignalR の構成」をご覧ください。 {OPTION} プレースホルダーがオプションを表し、{VALUE} プレースホルダーが値です。

Note

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

Program ファイルで次のように指定します。

builder.Services.AddRazorComponents().AddInteractiveServerComponents().AddHubOptions(options =>
{
    options.{OPTION} = {VALUE};
});

Program ファイルで次のように指定します。

builder.Services.AddServerSideBlazor().AddHubOptions(options =>
{
    options.{OPTION} = {VALUE};
});

Startup.csStartup.ConfigureServices で:

services.AddServerSideBlazor().AddHubOptions(options =>
{
    options.{OPTION} = {VALUE};
});

警告

MaximumReceiveMessageSize の既定値は 32 KB です。 この値を大きくすると、サービス拒否 (DoS) 攻撃のリスクが高まるおそれがあります。

Blazor は既定値である 1 に設定された MaximumParallelInvocationsPerClient を利用します。 詳細については、「MaximumParallelInvocationsPerClient > 1 によって Blazor Server モードでのファイル アップロードが中断される (dotnet/aspnetcore #53951)」を参照してください。

メモリ管理の詳細については、ASP.NET Core サーバー側 Blazor アプリのホストとデプロイに関する記事を参照してください。

Blazor ハブのオプション

MapBlazorHub のオプションを構成して、Blazor ハブの HttpConnectionDispatcherOptions を制御します。 参照ソースのハブ接続ディスパッチャー オプションの既定値を表示します。 {OPTION} プレースホルダーがオプションを表し、{VALUE} プレースホルダーが値です。

Note

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

アプリの Program ファイル内で app.MapRazorComponents 呼び出しの後に app.MapBlazorHub 呼び出しを配置します。

app.MapBlazorHub(options =>
{
    options.{OPTION} = {VALUE};
});

MapBlazorHubAddInteractiveServerRenderMode によって使用されるハブの構成は、AmbiguousMatchException で失敗します。

Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints.

.NET 8 を対象とするアプリの問題を回避するには、WithOrder メソッドを使用すると、カスタム構成 Blazor ハブの優先順位が高くなります。

app.MapBlazorHub(options =>
{
    options.CloseOnAuthenticationExpiration = true;
}).WithOrder(-1);

詳細については、次のリソースを参照してください。

アプリ Program のファイルに app.MapBlazorHub へのオプションを提供します。

app.MapBlazorHub(options =>
{
    options.{OPTION} = {VALUE};
});

エンドポイント ルーティング構成に app.MapBlazorHub へのオプションを提供します。

app.UseEndpoints(endpoints =>
{
    endpoints.MapBlazorHub(options =>
    {
        options.{OPTION} = {VALUE};
    });
    ...
});

最大受信メッセージ サイズ

このセクションは、SignalR を実装するプロジェクトにのみ適用されます。

ハブ メソッドで許可されている SignalR の最大受信メッセージ サイズは、HubOptions.MaximumReceiveMessageSize によって制限されます (既定値: 32 KB)。 SignalR のメッセージが MaximumReceiveMessageSize より大きい場合は、エラーがスローされます。 このフレームワークでは、ハブからクライアントへの SignalR メッセージのサイズが制限されることはありません。

SignalR のログがSignalR または Trace に設定されていない場合、メッセージ サイズのエラーはブラウザーの開発者ツール コンソールにのみ表示されます。

エラー :次のエラーで接続が切断されました。"エラー: サーバーが終了時にエラーを返しました:接続はエラーで終了しました。"

SignalR サーバー側のログ Debug または Trace に設定されている場合、サーバー側のログには、メッセージ サイズ エラーの InvalidDataException が表示されます。

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      ...
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

エラー:

System.IO.InvalidDataException:メッセージの最大サイズ 32,768 B を超えました。 メッセージのサイズは、AddHubOptions で構成できます。

1 つの方法として、Program ファイルで MaximumReceiveMessageSize を設定して制限を引き上げます。 次の例では、受信メッセージの最大サイズを 64 KB に設定します。

builder.Services.AddRazorComponents().AddInteractiveServerComponents()
    .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

SignalR の受信メッセージのサイズ制限を増やすと、サーバー リソースを増やす必要があり、サービス拒否 (DoS) 攻撃のリスクが高くなります。 また、大量のコンテンツを文字列またはバイト配列としてメモリに読み取ると、ガベージ コレクターがうまく機能しない割り当てが発生する可能性もあり、その結果、パフォーマンスがさらに低下します。

大きなペイロードを読み取るためより良い選択肢は、小さいチャンクでコンテンツを送信し、ペイロードを Stream として処理することです。 これは、大量の JavaScript (JS) 相互運用 JSON ペイロードを読み取る場合、または JS 相互運用データを生バイトとして利用できる場合に使用できます。 InputFile コンポーネントに似た手法を使ってサーバー側アプリで大規模なバイナリ ペイロードを送信する方法の例については、,Binary Submit サンプル アプリBlazorInputLargeTextArea コンポーネント サンプルを参照してください。

メモ

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

SignalR で大きなペイロードを処理するフォームでは、ストリーミング JS 相互運用を直接使用することもできます。 詳細については、「ASP.NET Core Blazor で JavaScript 関数から .NET メソッドを呼び出す」を参照してください。 <textarea> データをサーバーにストリーミングするフォームの例については、ASP.NET Core Blazor のフォームのトラブルシューティングに関する記事を参照してください。

1 つの方法として、Program ファイルで MaximumReceiveMessageSize を設定して制限を引き上げます。 次の例では、受信メッセージの最大サイズを 64 KB に設定します。

builder.Services.AddServerSideBlazor()
    .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

SignalR の受信メッセージのサイズ制限を増やすと、サーバー リソースを増やす必要があり、サービス拒否 (DoS) 攻撃のリスクが高くなります。 また、大量のコンテンツを文字列またはバイト配列としてメモリに読み取ると、ガベージ コレクターがうまく機能しない割り当てが発生する可能性もあり、その結果、パフォーマンスがさらに低下します。

大きなペイロードを読み取るためより良い選択肢は、小さいチャンクでコンテンツを送信し、ペイロードを Stream として処理することです。 これは、大量の JavaScript (JS) 相互運用 JSON ペイロードを読み取る場合、または JS 相互運用データを生バイトとして利用できる場合に使用できます。 Blazor Server で大規模なバイナリ ペイロードを送信する、InputFile コンポーネントに似た手法を使用する方法の例については、Binary Submit サンプル アプリに関する記事、およびBlazorInputLargeTextArea コンポーネント サンプルに関する記事を参照してください。

メモ

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

SignalR で大きなペイロードを処理するフォームでは、ストリーミング JS 相互運用を直接使用することもできます。 詳細については、「ASP.NET Core Blazor で JavaScript 関数から .NET メソッドを呼び出す」を参照してください。 Blazor Server アプリで <textarea> データをストリーミングするフォームの例については、ASP.NET Core Blazor のフォームのトラブルシューティングに関する記事を参照してください。

制限値を増やすには、Startup.ConfigureServicesMaximumReceiveMessageSize を設定します。

services.AddServerSideBlazor()
    .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

SignalR の受信メッセージのサイズ制限を増やすと、サーバー リソースを増やす必要があり、サービス拒否 (DoS) 攻撃のリスクが高くなります。 また、大量のコンテンツを文字列またはバイト配列としてメモリに読み取ると、ガベージ コレクターがうまく機能しない割り当てが発生する可能性もあり、その結果、パフォーマンスがさらに低下します。

大量のデータを転送するコードを開発するときは、次のガイダンスを考慮してください。

  • SignalR の受信メッセージ サイズの制限を超えるデータを転送するには、ネイティブ ストリーミング JS 相互運用サポートを利用します。
  • 一般的なヒント:
    • JS および C# コードで大きなオブジェクトを割り当てないでください。
    • プロセスの完了時またはキャンセル時に、消費していたメモリを解放します。
    • セキュリティ上の理由から、次の追加要件を適用します。
      • 渡すことのできるファイルまたはデータの最大サイズを宣言します。
      • クライアントからサーバーへの最小アップロード レートを宣言します。
    • データがサーバーによって受信されたら、データは:
      • すべてのセグメントが収集されるまで、一時的にメモリ バッファーに格納できます。
      • 直ちに消費できます。 たとえば、データは、データベースに直ちに格納することも、セグメントを受信するたびにディスクに書き込むこともできます。
  • データをより小さな部分にスライスし、すべてのデータがサーバーによって受信されるまでデータ セグメントを順番に送信します。
  • JS および C# コードで大きなオブジェクトを割り当てないでください。
  • データを送受信するときに、メイン UI スレッドを長時間ブロックしないでください。
  • プロセスの完了時またはキャンセル時に、消費していたメモリを解放します。
  • セキュリティ上の理由から、次の追加要件を適用します。
    • 渡すことのできるファイルまたはデータの最大サイズを宣言します。
    • クライアントからサーバーへの最小アップロード レートを宣言します。
  • データがサーバーによって受信されたら、データは:
    • すべてのセグメントが収集されるまで、一時的にメモリ バッファーに格納できます。
    • 直ちに消費できます。 たとえば、データは、データベースに直ちに格納することも、セグメントを受信するたびにディスクに書き込むこともできます。

Blazor サーバー側 Hub のエンドポイント ルート構成

Program ファイルで MapBlazorHub を呼び出して、BlazorHub をアプリの既定のパスにマップします。 Blazor スクリプト (blazor.*.js) は、MapBlazorHub によって作成されたエンドポイントを自動的に指します。

UI にサーバー側の接続状態を反映する

クライアントがサーバーへの接続を失ったと検出した場合、クライアントが再接続を試みる間、既定の UI がユーザーに表示されます。

既定の再接続 UI。

既定の再接続 UI。

再接続に失敗した場合、ユーザーはページを再試行または再読み込みするように指示されます。

既定の再試行 UI。

既定の再試行 UI。

再接続に成功すると、多くの場合、ユーザーの状態が失われます。 カスタム コードを任意のコンポーネントに追加して、接続エラーの間にユーザーの状態を保存および再読み込みできます。 詳細については、「ASP.NET Core Blazor 状態管理」を参照してください。

UI をカスタマイズするには、<body>要素コンテンツにcomponents-reconnect-modalidを持つ 1 つの要素を定義します。 次の例では、App コンポーネントに要素を配置します。

App.razor:

UI をカスタマイズするには、<body>要素コンテンツにcomponents-reconnect-modalidを持つ 1 つの要素を定義します。 次の例では、ホスト ページに要素を配置します。

Pages/_Host.cshtml:

UI をカスタマイズするには、<body>要素コンテンツにcomponents-reconnect-modalidを持つ 1 つの要素を定義します。 次の例では、レイアウト ページに要素を配置します。

Pages/_Layout.cshtml:

UI をカスタマイズするには、<body>要素コンテンツにcomponents-reconnect-modalidを持つ 1 つの要素を定義します。 次の例では、ホスト ページに要素を配置します。

Pages/_Host.cshtml:

<div id="components-reconnect-modal">
    Connection lost.<br>Attempting to reconnect...
</div>

注意

components-reconnect-modalid を持つ複数の要素がアプリによってレンダリングされる場合、最初にレンダリングされた要素のみが CSS クラスの変更を受け取り、要素を表示または非表示にします。

次の CSS スタイルをサイトのスタイルシートに追加します。

wwwroot/app.css:

wwwroot/css/site.css:

#components-reconnect-modal {
    display: none;
}

#components-reconnect-modal.components-reconnect-show, 
#components-reconnect-modal.components-reconnect-failed, 
#components-reconnect-modal.components-reconnect-rejected {
    display: block;
    background-color: white;
    padding: 2rem;
    border-radius: 0.5rem;
    text-align: center;
    box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
    margin: 50px 50px;
    position: fixed;
    top: 0;
    z-index: 10001;
}

次の表で、Blazor フレームワークによってcomponents-reconnect-modal 要素に適用される CSS クラスについて説明します。

CSS クラス 示す内容...
components-reconnect-show 接続が失われました。 クライアントによって再接続が試行されています。 モーダルを表示します。
components-reconnect-hide サーバーへのアクティブな接続が再確立されます。 モーダルを非表示にします。
components-reconnect-failed 再接続に失敗しました。ネットワーク障害が原因である可能性があります。 再接続を試みるには、JavaScript で window.Blazor.reconnect() を呼び出します。
components-reconnect-rejected 再接続が拒否されました。 サーバーに到達したが接続が拒否されたため、サーバー上のユーザーの状態が失われました。 アプリを再度読み込むには、JavaScript で location.reload() を呼び出します。 この接続状態は、次の場合に発生する可能性があります。
  • サーバー側回線でクラッシュが発生した場合。
  • クライアントが長時間切断されているため、サーバーでユーザーの状態が削除された場合。 ユーザーのコンポーネントのインスタンスは破棄されます。
  • サーバーが再起動されたか、アプリのワーカー プロセスがリサイクルされた場合。

モーダル要素に対して、サイトの CSS で transition-delay プロパティを設定して、再接続 UI が表示されるまでの遅延時間をカスタマイズします。 次の例では、移行遅延時間を 500 ms (既定値) から 1,000 ms (1 秒) に設定しています。

wwwroot/app.css:

wwwroot/css/site.css:

#components-reconnect-modal {
    transition: visibility 0s linear 1000ms;
}

現在の再接続の試行を表示するには、components-reconnect-current-attemptid を使って要素を定義します。 再接続の再試行の最大数を表示するには、components-reconnect-max-retriesid を使って要素を定義します。 次の例では、前の例に従って、これらの要素を再接続試行モーダル要素内に配置します。

<div id="components-reconnect-modal">
    There was a problem with the connection!
    (Current reconnect attempt: 
    <span id="components-reconnect-current-attempt"></span> /
    <span id="components-reconnect-max-retries"></span>)
</div>

カスタム再接続モーダルが表示されると、再接続試行カウンターを使用して次のコンテンツがレンダリングされます。

接続に問題が発生しました! (現在の再接続の試行: 1 / 8)

サーバー側のレンダリング

既定では、サーバーへのクライアント接続が確立される前に、コンポーネントがサーバー上でプリレンダリングされます。 詳細については、「ASP.NET Core Razor のプリレンダリングとコンポーネント」をご覧ください。

既定では、サーバーへのクライアント接続が確立される前に、コンポーネントがサーバー上でプリレンダリングされます。 詳細については、「ASP.NET Core のコンポーネント タグ ヘルパー」を参照してください。

サーバー側の回線アクティビティを監視する

CircuitHandler 上で CreateInboundActivityHandler メソッドを使って、受信回線アクティビティを監視します。 受信回線アクティビティとは、UI イベントや JavaScript から .NET への相互運用呼び出しなど、ブラウザーからサーバーに送信されるすべてのアクティビティです。

たとえば、回線アクティビティ ハンドラーを使って、クライアントがアイドル状態かどうかとそのサーキット ID (Circuit.Id) を検出できます:

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.Options;
using Timer = System.Timers.Timer;

public sealed class IdleCircuitHandler : CircuitHandler, IDisposable
{
    private Circuit? currentCircuit;
    private readonly ILogger logger;
    private readonly Timer timer;

    public IdleCircuitHandler(ILogger<IdleCircuitHandler> logger, 
        IOptions<IdleCircuitOptions> options)
    {
        timer = new Timer
        {
            Interval = options.Value.IdleTimeout.TotalMilliseconds,
            AutoReset = false
        };

        timer.Elapsed += CircuitIdle;
        this.logger = logger;
    }

    private void CircuitIdle(object? sender, System.Timers.ElapsedEventArgs e)
    {
        logger.LogInformation("{CircuitId} is idle", currentCircuit?.Id);
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        currentCircuit = circuit;

        return Task.CompletedTask;
    }

    public override Func<CircuitInboundActivityContext, Task> CreateInboundActivityHandler(
        Func<CircuitInboundActivityContext, Task> next)
    {
        return context =>
        {
            timer.Stop();
            timer.Start();

            return next(context);
        };
    }

    public void Dispose() => timer.Dispose();
}

public class IdleCircuitOptions
{
    public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(5);
}

public static class IdleCircuitHandlerServiceCollectionExtensions
{
    public static IServiceCollection AddIdleCircuitHandler(
        this IServiceCollection services, 
        Action<IdleCircuitOptions> configureOptions)
    {
        services.Configure(configureOptions);
        services.AddIdleCircuitHandler();

        return services;
    }

    public static IServiceCollection AddIdleCircuitHandler(
        this IServiceCollection services)
    {
        services.AddScoped<CircuitHandler, IdleCircuitHandler>();

        return services;
    }
}

Program ファイルにサービスを登録します。 次の例では、上記の IdleCircuitHandler 実装をテストするために、5 分から 5 秒の既定のアイドル タイムアウトを構成します:

builder.Services.AddIdleCircuitHandler(options => 
    options.IdleTimeout = TimeSpan.FromSeconds(5));

また、回線アクティビティ ハンドラーには、他の Blazor ではない依存関係の挿入 (DI) スコープからスコープ付き Blazor サービスにアクセスするためのアプローチも用意されています。 詳細と例については、次をご覧ください。

Blazor の起動

Blazor Web AppのApp.razor ファイルで、BlazorのSignalR回線の手動開始を構成します。

Pages/_Host.cshtml ファイル (Blazor Server) で、BlazorのSignalR回線の手動開始を構成します。

Pages/_Layout.cshtml ファイル (Blazor Server) で、BlazorのSignalR回線の手動開始を構成します。

Pages/_Host.cshtml ファイル (Blazor Server) で、BlazorのSignalR回線の手動開始を構成します。

  • blazor.*.js スクリプトの <script> タグに autostart="false" 属性を追加します。
  • Blazor.start() を呼び出すスクリプトを、Blazor スクリプトが読み込まれた後の終了 </body> タグ内に配置します。

autostart が無効になっている場合、回線に依存しないアプリのすべての側面が正常に動作します。 たとえば、クライアント側のルーティングは動作します。 ただし、回線に依存する側面はすべて、Blazor.start() が呼び出されるまで動作しません。 回線が確立されていなければ、アプリの動作は予測不可能です。 たとえば、回線が切断されている間、コンポーネント メソッドは実行できません。

ドキュメントの準備が完了したときに Blazor を初期化する方法や JS Promise に連結する方法を含む詳細については、ASP.NET Core Blazor の起動に関する記事を参照してください。

クライアントで SignalR タイムアウトと Keep-Alive を構成する

クライアントに対して次の値を構成します。

  • withServerTimeout: サーバーのタイムアウト (ミリ秒単位) を構成します。 サーバーからメッセージを受信せずにこのタイムアウトが経過すると、接続はエラーで終了します。 タイムアウトの既定値は 30 秒です。 サーバー タイムアウトは、Keep-Alive 間隔 (withKeepAliveInterval) に割り当てられた値の少なくとも 2 倍にする必要があります。
  • withKeepAliveInterval: Keep-Alive 間隔 (サーバーに ping を実行する既定の間隔) をミリ秒単位で構成します。 この設定により、サーバーでハードの切断を検出できます。たとえば、クライアントがコンピューターをネットワークから取り外したときなどです。 ping は、最大でサーバーの ping と同じ頻度で発生します。 サーバーが 5 秒ごとに ping を実行する場合、5000 (5 秒) 未満の値を割り当てると 5 秒ごとに ping が実行されます。 既定値は 15 秒です。 Keep-Alive 間隔は、サーバー タイムアウト (withServerTimeout) に割り当てられた値の半分以下にする必要があります。

App.razor ファイル (Blazor Web App) の次の例は、既定値の代入を示しています。

Blazor Web App:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      configureSignalR: function (builder) {
        builder.withServerTimeout(30000).withKeepAliveInterval(15000);
      }
    }
  });
</script>

Pages/_Host.cshtml ファイル (Blazor Server、.NET 6 の ASP.NET Core を除くすべてのバージョン) または Pages/_Layout.cshtml ファイル (Blazor Server、.NET 6 の ASP.NET Core) の例を次に示します。

Blazor Server:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      builder.withServerTimeout(30000).withKeepAliveInterval(15000);
    }
  });
</script>

前の例の {BLAZOR SCRIPT} プレースホルダーは、Blazor スクリプトのパスとファイル名です。 スクリプトの場所と使用するパスについては、「ASP.NET Core Blazor プロジェクトの構造」をご覧ください。

コンポーネントでハブ接続を作成する場合は、HubConnectionBuilderServerTimeout (既定値: 30 秒) と KeepAliveInterval (既定値: 15 秒) を設定します。 ビルドされた HubConnectionHandshakeTimeout (既定値: 15 秒) を設定します。 次の例は、既定値の代入を示しています。

protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .WithServerTimeout(TimeSpan.FromSeconds(30))
        .WithKeepAliveInterval(TimeSpan.FromSeconds(15))
        .Build();

    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(15);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}

クライアントに対して次の値を構成します。

  • serverTimeoutInMilliseconds: サーバーのタイムアウト (ミリ秒単位)。 サーバーからメッセージを受信せずにこのタイムアウトが経過すると、接続はエラーで終了します。 タイムアウトの既定値は 30 秒です。 サーバー タイムアウトは、Keep-Alive 間隔 (keepAliveIntervalInMilliseconds) に割り当てられた値の少なくとも 2 倍にする必要があります。
  • keepAliveIntervalInMilliseconds: サーバーに ping を実行する既定の間隔。 この設定により、サーバーでハードの切断を検出できます。たとえば、クライアントがコンピューターをネットワークから取り外したときなどです。 ping は、最大でサーバーの ping と同じ頻度で発生します。 サーバーが 5 秒ごとに ping を実行する場合、5000 (5 秒) 未満の値を割り当てると 5 秒ごとに ping が実行されます。 既定値は 15 秒です。 Keep-Alive 間隔は、サーバー タイムアウト (serverTimeoutInMilliseconds) に割り当てられた値の半分以下にする必要があります。

Pages/_Host.cshtml ファイル (Blazor Server、.NET 6 の ASP.NET Core を除くすべてのバージョン) または Pages/_Layout.cshtml ファイル (Blazor Server、.NET 6 の ASP.NET Core) の例を次に示します。

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      let c = builder.build();
      c.serverTimeoutInMilliseconds = 30000;
      c.keepAliveIntervalInMilliseconds = 15000;
      builder.build = () => {
        return c;
      };
    }
  });
</script>

前の例の {BLAZOR SCRIPT} プレースホルダーは、Blazor スクリプトのパスとファイル名です。 スクリプトの場所と使用するパスについては、「ASP.NET Core Blazor プロジェクトの構造」をご覧ください。

コンポーネントでハブ接続を作成する場合は、ビルドされた HubConnectionServerTimeout (既定値: 30 秒)、HandshakeTimeout (既定値: 15 秒)、KeepAliveInterval (既定値: 15 秒) を設定します。 次の例は、既定値の代入を示しています。

protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .Build();

    hubConnection.ServerTimeout = TimeSpan.FromSeconds(30);
    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(15);
    hubConnection.KeepAliveInterval = TimeSpan.FromSeconds(15);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}

サーバー タイムアウト (ServerTimeout) または Keep-Alive 間隔 (KeepAliveInterval) の値を変更する場合:

  • サーバー タイムアウトは、Keep-Alive 間隔に割り当てられた値の少なくとも 2 倍にする必要があります。
  • Keep-Alive 間隔は、サーバー タイムアウトに割り当てられた値の半分以下にする必要があります。

詳細については、以下の記事の「グローバル展開と接続エラー」のセクションを参照してください。

サーバー側の再接続ハンドラーを変更する

再接続ハンドラーの回線接続イベントは、次のようなカスタム動作を行うように変更できます。

  • 接続が切断された場合にユーザーに通知する。
  • 回線が接続されているときに (クライアントから) ログ記録を実行する。

接続イベントを変更するには、次の接続の変更に対してコールバックを登録します。

  • 切断された接続では、onConnectionDown が使用されます。
  • 確立または再確立された接続では、onConnectionUp が使用されます。

onConnectionDownonConnectionUp の両方を指定する必要があります。

Blazor Web App:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      reconnectionHandler: {
        onConnectionDown: (options, error) => console.error(error),
        onConnectionUp: () => console.log("Up, up, and away!")
      }
    }
  });
</script>

Blazor Server:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    reconnectionHandler: {
      onConnectionDown: (options, error) => console.error(error),
      onConnectionUp: () => console.log("Up, up, and away!")
    }
  });
</script>

前の例の {BLAZOR SCRIPT} プレースホルダーは、Blazor スクリプトのパスとファイル名です。 スクリプトの場所と使用するパスについては、「ASP.NET Core Blazor プロジェクトの構造」をご覧ください。

サーバー側の再接続が失敗したときにページを自動的に更新する

既定の再接続動作では、再接続が失敗した後にページを更新するための手動アクションをユーザーが行う必要があります。 ただし、カスタム再接続ハンドラーを使用すると、ページを自動的に更新できます。

App.razor:

Pages/_Host.cshtml:

<div id="reconnect-modal" style="display: none;"></div>
<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script src="boot.js"></script>

前の例の {BLAZOR SCRIPT} プレースホルダーは、Blazor スクリプトのパスとファイル名です。 スクリプトの場所と使用するパスについては、「ASP.NET Core Blazor プロジェクトの構造」をご覧ください。

次の wwwroot/boot.js ファイルを作成します。

Blazor Web App:

(() => {
  const maximumRetryCount = 3;
  const retryIntervalMilliseconds = 5000;
  const reconnectModal = document.getElementById('reconnect-modal');

  const startReconnectionProcess = () => {
    reconnectModal.style.display = 'block';

    let isCanceled = false;

    (async () => {
      for (let i = 0; i < maximumRetryCount; i++) {
        reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`;

        await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));

        if (isCanceled) {
          return;
        }

        try {
          const result = await Blazor.reconnect();
          if (!result) {
            // The server was reached, but the connection was rejected; reload the page.
            location.reload();
            return;
          }

          // Successfully reconnected to the server.
          return;
        } catch {
          // Didn't reach the server; try again.
        }
      }

      // Retried too many times; reload the page.
      location.reload();
    })();

    return {
      cancel: () => {
        isCanceled = true;
        reconnectModal.style.display = 'none';
      },
    };
  };

  let currentReconnectionProcess = null;

  Blazor.start({
    circuit: {
      reconnectionHandler: {
        onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(),
        onConnectionUp: () => {
          currentReconnectionProcess?.cancel();
          currentReconnectionProcess = null;
        }
      }
    }
  });
})();

Blazor Server:

(() => {
  const maximumRetryCount = 3;
  const retryIntervalMilliseconds = 5000;
  const reconnectModal = document.getElementById('reconnect-modal');

  const startReconnectionProcess = () => {
    reconnectModal.style.display = 'block';

    let isCanceled = false;

    (async () => {
      for (let i = 0; i < maximumRetryCount; i++) {
        reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`;

        await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));

        if (isCanceled) {
          return;
        }

        try {
          const result = await Blazor.reconnect();
          if (!result) {
            // The server was reached, but the connection was rejected; reload the page.
            location.reload();
            return;
          }

          // Successfully reconnected to the server.
          return;
        } catch {
          // Didn't reach the server; try again.
        }
      }

      // Retried too many times; reload the page.
      location.reload();
    })();

    return {
      cancel: () => {
        isCanceled = true;
        reconnectModal.style.display = 'none';
      },
    };
  };

  let currentReconnectionProcess = null;

  Blazor.start({
    reconnectionHandler: {
      onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(),
      onConnectionUp: () => {
        currentReconnectionProcess?.cancel();
        currentReconnectionProcess = null;
      }
    }
  });
})();

Blazor の起動の詳細については、「ASP.NET Core Blazor の起動」をご覧ください。

サーバー側の再接続の再試行回数と間隔を調整する

再接続の再試行の回数と間隔を調整するには、再試行の回数 (maxRetries) と、各再試行で許可されるミリ秒単位の期間 (retryIntervalMilliseconds) を設定します。

Blazor Web App:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      reconnectionOptions: {
        maxRetries: 3,
        retryIntervalMilliseconds: 2000
      }
    }
  });
</script>

Blazor Server:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    reconnectionOptions: {
      maxRetries: 3,
      retryIntervalMilliseconds: 2000
    }
  });
</script>

前の例の {BLAZOR SCRIPT} プレースホルダーは、Blazor スクリプトのパスとファイル名です。 スクリプトの場所と使用するパスについては、「ASP.NET Core Blazor プロジェクトの構造」をご覧ください。

ユーザーが、回線が切断されたアプリに戻ると、次の再接続間隔を待たずに、すぐに再接続が試行されます。 この動作は、ユーザーが接続をできるだけ早く再開できるようにすることを目的としています。

既定の再接続のタイミングでは、計算されたバックオフ戦略が使用されます。 試行間に計算された遅延が採用されるまで、最初の数回は短い間隔で再接続が試行されます。 再試行間隔を計算するための既定のロジックの実装の詳細は、予告なしに変更される可能性がありますが、Blazor フレームワークで使用される既定のロジックは、computeDefaultRetryInterval 関数 (参照ソース) で確認できます。

Note

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

再試行間隔を計算する関数を指定して、再試行間隔の動作をカスタマイズします。 次のエクスポネンシャル バックオフの例では、以前の再接続試行回数に 1,000 ミリ秒を掛けて再試行間隔を計算します。 以前の再接続試行回数 (previousAttempts) が再試行回数の上限 (maxRetries) より大きい場合、再試行間隔 (retryIntervalMilliseconds) に null が割り当てられ、それ以降の再接続試行は停止されます。

Blazor.start({
  circuit: {
    reconnectionOptions: {
      retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
        previousAttempts >= maxRetries ? null : previousAttempts * 1000
    },
  },
});

別の方法として、再試行間隔の正確なシーケンスを指定します。 最後に指定された再試行間隔の後、retryIntervalMilliseconds 関数は undefined を返すため再試行は停止します。

Blazor.start({
  circuit: {
    reconnectionOptions: {
      retryIntervalMilliseconds: 
        Array.prototype.at.bind([0, 1000, 2000, 5000, 10000, 15000, 30000]),
    },
  },
});

Blazor の起動の詳細については、「ASP.NET Core Blazor の起動」をご覧ください。

再接続 UI が表示されるタイミングを制御する

再接続 UI が表示されるタイミングを制御すると、次のような場合に役立ちます。

  • デプロイされたアプリでは、内部ネットワークまたはインターネットの待機時間によって ping タイムアウトが発生したため、再接続 UI が頻繁に表示されるので、延期期間を増やしたい場合。
  • あるアプリで、接続が早く切断されたことをユーザーに報告する必要があるため、延期期間を短縮したい場合。

再接続 UI の出現のタイミングは、クライアントのキープアライブ間隔とタイムアウトの調整による影響を受けます。 再接続 UI は、クライアント (withServerTimeout[クライアント構成] セクション) でサーバー タイムアウトに達すると表示されます。 ただし、withServerTimeout の値を変更するには、次のガイダンスで説明されている他のキープアライブ設定、タイムアウト設定、ハンドシェイク設定を変更する必要があります。

一般的な推奨事項として次のようなガイダンスがあります。

  • キープアライブ間隔は、クライアントとサーバーの構成が一致する必要があります。
  • タイムアウトは、キープアライブ間隔に割り当てられた値の 2 倍以上である必要があります。

サーバー構成

次のように設定します。

  • ClientTimeoutInterval (既定値: 30 秒): 時間枠クライアントは、サーバーが接続を閉じる前にメッセージを送信する必要があります。
  • HandshakeTimeout (既定値: 15 秒): クライアントによる着信ハンドシェイク要求のタイムアウトにサーバーで使用される間隔。
  • KeepAliveInterval (既定値: 15 秒): 接続済みクライアントにキープ アライブ ping を送信するためにサーバーで使用される間隔。 クライアントにもキープアライブ間隔の設定があり、サーバーの値と一致する必要があることに注意してください。

ClientTimeoutIntervalHandshakeTimeout を増やすことができ、KeepAliveInterval はそのままでかまいません。 重要な考慮事項として、値を変更する場合は、タイムアウトがキープアライブ間隔の値の 2 倍以上になるようにし、またキープアライブ間隔はクライアントとサーバー間で一致する必要があります。 詳細については、「クライアントで SignalR タイムアウトと Keep-Alive を構成する」セクションを参照してください。

次に例を示します。

  • ClientTimeoutInterval を、60 秒に増やします (既定値: 30 秒)。
  • HandshakeTimeout を、30 秒 (既定値: 15 秒) に増やします。
  • KeepAliveInterval は開発者コードでは設定されていないので、既定値の 15 秒が使用されます。 キープアライブ間隔の値を小さくすると、通信 ping の頻度が増加し、アプリ、サーバー、ネットワークの負荷が増加します。 キープアライブ間隔を短縮する場合は、パフォーマンスが低下しないように注意する必要があります。

サーバー プロジェクトの Program ファイル内の Blazor Web App (.NET 8 以降):

builder.Services.AddRazorComponents().AddInteractiveServerComponents()
    .AddHubOptions(options =>
{
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
    options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});

Program ファイル内の Blazor Server:

builder.Services.AddServerSideBlazor()
    .AddHubOptions(options =>
    {
        options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
        options.HandshakeTimeout = TimeSpan.FromSeconds(30);
    });

詳細については、「サーバー側の回線ハンドラーのオプション」セクションを参照してください。

クライアントの構成

次のように設定します。

  • withServerTimeout (既定値: 30 秒): 回線のハブ接続に対して、ミリ秒単位で指定されたサーバー タイムアウトを構成します。
  • withKeepAliveInterval (既定値: 15 秒): 接続がキープアライブ メッセージを送信する間隔 (ミリ秒単位で指定)。

サーバーのタイムアウトを増やすことができます。キープアライブ間隔はそのままにすることができます。 重要な考慮事項として、値を変更する場合は、サーバーのタイムアウトがキープアライブ間隔の値の 2 倍以上になるようにし、またキープアライブ間隔の値はクライアントとサーバー間で一致する必要があります。 詳細については、「クライアントで SignalR タイムアウトと Keep-Alive を構成する」セクションを参照してください。

次のスタートアップ構成の例 (Blazorスクリプトの場所) では、サーバーのタイムアウトに 60 秒のカスタム値が使用されています。 キープアライブ間隔 (withKeepAliveInterval) は設定されておらず、既定値の 15 秒を使用します。

Blazor Web App:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    circuit: {
      configureSignalR: function (builder) {
        builder.withServerTimeout(60000);
      }
    }
  });
</script>

Blazor Server:

<script src="{BLAZOR SCRIPT}" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      builder.withServerTimeout(60000);
    }
  });
</script>

コンポーネントでハブ接続を作成する場合は、構築された HubConnectionBuilder にサーバー タイムアウト (WithServerTimeout、既定値: 30 秒) を設定します。 ビルドされた HubConnectionHandshakeTimeout (既定値: 15 秒) を設定します。 タイムアウトがキープアライブ間隔 (WithKeepAliveInterval/KeepAliveInterval) の 2 倍以上であり、キープアライブ値がサーバーとクライアントの間で一致していることを確認します。

次の例は、Blazor を使用した SignalR のチュートリアルIndex コンポーネントに基づいています。 サーバー タイムアウトを 60 秒に増やし、ハンドシェイク タイムアウトを 30 秒に増やしています。 キープアライブ間隔は設定されておらず、既定値の 15 秒を使用します。

protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .WithServerTimeout(TimeSpan.FromSeconds(60))
        .Build();

    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}

次のように設定します。

  • serverTimeoutInMilliseconds (既定値: 30 秒): 回線のハブ接続に対して、ミリ秒単位で指定されたサーバー タイムアウトを構成します。
  • keepAliveIntervalInMilliseconds (既定値: 15 秒): 接続がキープアライブ メッセージを送信する間隔 (ミリ秒単位で指定)。

サーバーのタイムアウトを増やすことができます。キープアライブ間隔はそのままにすることができます。 重要な考慮事項として、値を変更する場合は、サーバーのタイムアウトがキープアライブ間隔の値の 2 倍以上になるようにし、またキープアライブ間隔の値はクライアントとサーバー間で一致する必要があります。 詳細については、「クライアントで SignalR タイムアウトと Keep-Alive を構成する」セクションを参照してください。

次のスタートアップ構成の例 (Blazorスクリプトの場所) では、サーバーのタイムアウトに 60 秒のカスタム値が使用されています。 キープアライブ間隔 (keepAliveIntervalInMilliseconds) は設定されておらず、既定値の 15 秒を使用します。

Pages/_Host.cshtml:

<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
  Blazor.start({
    configureSignalR: function (builder) {
      let c = builder.build();
      c.serverTimeoutInMilliseconds = 60000;
      builder.build = () => {
        return c;
      };
    }
  });
</script>

コンポーネントでハブ接続を作成する場合は、ビルドされた HubConnectionServerTimeout (既定値: 30 秒) と HandshakeTimeout (既定値: 15 秒) を設定します。 タイムアウトがキープアライブ間隔の 2 倍以上であることを確認します。 キープアライブ間隔がサーバーとクライアントの間で一致することを確認します。

次の例は、Blazor を使用した SignalR のチュートリアルIndex コンポーネントに基づいています。 次の例では、ServerTimeout を 60 秒に増やし、HandshakeTimeout を 30 秒に増やしています。 キープアライブ間隔 (KeepAliveInterval) は設定されておらず、既定値の 15 秒を使用します。

protected override async Task OnInitializedAsync()
{
    hubConnection = new HubConnectionBuilder()
        .WithUrl(Navigation.ToAbsoluteUri("/chathub"))
        .Build();

    hubConnection.ServerTimeout = TimeSpan.FromSeconds(60);
    hubConnection.HandshakeTimeout = TimeSpan.FromSeconds(30);

    hubConnection.On<string, string>("ReceiveMessage", (user, message) => ...

    await hubConnection.StartAsync();
}

BlazorのSignalR回線をクライアントから切断する

Blazorの SignalR 回線は、 unload ページ イベント がトリガーされると切断されます。 クライアント上の他のシナリオで回線を切断するには、適切なイベント ハンドラーで Blazor.disconnect を呼び出します。 次の例では、ページが非表示になると、回線が切断されます (pagehide イベント)。

window.addEventListener('pagehide', () => {
  Blazor.disconnect();
});

Blazor の起動の詳細については、「ASP.NET Core Blazor の起動」をご覧ください。

サーバー側の回線ハンドラー

ユーザー回線の状態変化時にコードを実行できる "回線ハンドラー" を定義できます。 回線ハンドラーは、CircuitHandler から派生させ、そのクラスをアプリのサービス コンテナーに登録することで実装します。 次の回線ハンドラーの例では、開いている SignalR 接続を追跡します。

TrackingCircuitHandler.cs:

using Microsoft.AspNetCore.Components.Server.Circuits;

public class TrackingCircuitHandler : CircuitHandler
{
    private HashSet<Circuit> circuits = new();

    public override Task OnConnectionUpAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        circuits.Add(circuit);

        return Task.CompletedTask;
    }

    public override Task OnConnectionDownAsync(Circuit circuit, 
        CancellationToken cancellationToken)
    {
        circuits.Remove(circuit);

        return Task.CompletedTask;
    }

    public int ConnectedCircuits => circuits.Count;
}

回線ハンドラーは DI を使用して登録されます。 スコープを持つインスタンスは、回線のインスタンスごとに作成されます。 前の例の TrackingCircuitHandler を使用すると、すべての回線の状態を追跡する必要があるため、シングルトン サービスが作成されます。

Program ファイルで次のように指定します。

builder.Services.AddSingleton<CircuitHandler, TrackingCircuitHandler>();

Startup.csStartup.ConfigureServices で:

services.AddSingleton<CircuitHandler, TrackingCircuitHandler>();

カスタム回線ハンドラーのメソッドでハンドルされない例外がスローされる場合は、その例外は 回線にとって致命的です。 ハンドラーのコードまたはメソッドで例外が許容されるようにするには、エラー処理とログを含む 1 つ以上の try-catch ステートメントでコードをラップします。

ユーザーが切断し、フレームワークで回線の状態がクリーンアップされていることが原因で回線が終了すると、フレームワークによって回線の DI スコープが破棄されます。 スコープが破棄されると、System.IDisposable を実装するサーキットスコープの DI サービスはすべて破棄されます。 破棄中にいずれかの DI サービスでハンドルされない例外がスローされると、フレームワークによって例外がログに記録されます。 詳細については、「ASP.NET Core Blazor の依存関係の挿入」を参照してください。

カスタム サービスのユーザーをキャプチャするためのサーバー側回線ハンドラー

AuthenticationStateProvider からユーザーをキャプチャして、サービスでそのユーザーを設定するには、CircuitHandler を使います。 詳細とコード例については、「コア サーバー側の ASP.NET および追加のセキュリティ シナリオ Blazor Web App 参照してください

対話型サーバー コンポーネントが残っていない場合に回線を閉じる

対話型サーバー コンポーネントでは、ブラウザーとのリアルタイム接続 (回線) を使用して、Web UI イベントを処理します。 ルートの対話型サーバー コンポーネントがレンダリングされる際、回線およびその関連付けられた状態が作成されます。 ページ上に対話型サーバー コンポーネントが残っていない場合、回線は閉じられ、サーバー リソースは解放されます。

Razor コンポーネント内の IHttpContextAccessor/HttpContext

有効な HttpContext が使用できないため、対話型レンダリングでは IHttpContextAccessor を避ける必要があります。

IHttpContextAccessor は、サーバー上で静的にレンダリングされるコンポーネントに対して使用することができます。 ただし、可能であれば使用を避けることをお勧めします。

HttpContext は、App コンポーネント (Components/App.razor) 内のヘッダーやその他のプロパティの検査や変更などの一般的なタスク用に、"静的にレンダリングされたルート コンポーネント" 内でのみ、カスケード パラメーターとして使用できます。 対話型レンダリングの場合、この値は常に null です。

[CascadingParameter]
public HttpContext? HttpContext { get; set; }

対話型コンポーネント内で HttpContext が必要なシナリオの場合は、サーバーから永続的なコンポーネントの状態を介してデータを取り込むことをお勧めします。 詳細については、コア サーバー側 ASP.NET および追加のセキュリティ シナリオ Blazor Web App 参照してください

サーバー側 Blazor アプリの Razor コンポーネントでは、IHttpContextAccessor/HttpContext を、直接にも間接的にも使用しないでください。 Blazor アプリは、ASP.NET Core パイプラインのコンテキストの外部で実行されます。 HttpContext は、IHttpContextAccessor 内で使用できるとは限りません。また、HttpContext は、Blazor アプリを開始したコンテキストが保持されることも保証されません。

アプリの初期レンダリング中にルート コンポーネント パラメーターを使って要求の状態を Blazor アプリに渡すことをお勧めします。 または、ルート コンポーネントの初期化ライフサイクル イベントにおいてアプリでスコープ サービスにデータをコピーすることで、アプリ全体で使用することもできます。 詳細については、コア サーバー側 ASP.NET および追加のセキュリティ シナリオ Blazor Web App 参照してください

サーバー側 Blazor のセキュリティでの重要な側面は、特定の回線に接続されているユーザーは Blazor 回線が確立された後のある時点で更新される可能性がありますが、IHttpContextAccessor は "更新されない" ということです。 カスタム サービスを使用してこのような状況に対処する方法の詳細については、「コア サーバー側ASP.NET および追加のセキュリティ シナリオBlazor Web Appを参照してください。

その他のサーバー側リソース