ASP.NET Core Blazor アプリのエラーを処理する

注意

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

重要

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

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

この記事では、ハンドルされない例外を Blazor で管理する方法と、エラーを検出して処理するアプリを開発する方法について説明します。

開発中の詳細なエラー

開発中に Blazor アプリが正常に機能していない場合、アプリからの詳細なエラー情報を受け取ることで、問題のトラブルシューティングと修正に役立ちます。 エラーが発生すると、Blazor アプリによって画面の下部に薄い黄色のバーが表示されます。

  • 開発中は、バーによってブラウザー コンソールが表示され、そこで例外を確認できます。
  • 運用環境では、バーによって、エラーが発生したことがユーザーに通知され、ブラウザーの更新が推奨されます。

このエラー処理エクスペリエンスの UI は、Blazor プロジェクト テンプレートの一部です。 Blazor プロジェクト テンプレートのすべてのバージョンが、エラー UI の内容をキャッシュしないようにブラウザーに通知するために data-nosnippet 属性を使っているわけではありません。ただし、Blazor ドキュメントのすべてのバージョンがこの属性を適用しています。

Blazor Web アプリでは、MainLayout コンポーネントのエクスペリエンスをカスタマイズします。 環境タグ ヘルパー (<environment include="Production">...</environment> など) は Razor コンポーネントではサポートされていないため、次の例では IHostEnvironment を挿入して、さまざまな環境のエラー メッセージを構成します。

MainLayout.razorの上部:

@inject IHostEnvironment HostEnvironment

Blazor エラー UI マークアップを作成または変更します。

<div id="blazor-error-ui" data-nosnippet>
    @if (HostEnvironment.IsProduction())
    {
        <span>An error has occurred.</span>
    }
    else
    {
        <span>An unhandled exception occurred.</span>
    }
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Blazor Server アプリでは、Pages/_Host.cshtml ファイルでエクスペリエンスをカスタマイズします。 次の例では、環境タグ ヘルパーを使用して、さまざまな環境のエラー メッセージを構成します。

Blazor Server アプリでは、Pages/_Layout.cshtml ファイルでエクスペリエンスをカスタマイズします。 次の例では、環境タグ ヘルパーを使用して、さまざまな環境のエラー メッセージを構成します。

Blazor Server アプリでは、Pages/_Host.cshtml ファイルでエクスペリエンスをカスタマイズします。 次の例では、環境タグ ヘルパーを使用して、さまざまな環境のエラー メッセージを構成します。

Blazor エラー UI マークアップを作成または変更します。

<div id="blazor-error-ui" data-nosnippet>
    <environment include="Staging,Production">
        An error has occurred.
    </environment>
    <environment include="Development">
        An unhandled exception occurred.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Blazor WebAssembly アプリでは、wwwroot/index.html ファイルでエクスペリエンスをカスタマイズします。

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

blazor-error-ui 要素は、通常、アプリの自動生成スタイルシート に blazor-error-ui CSS クラスの display: none スタイルが存在するため、非表示になります。 エラーが発生すると、フレームワークによって要素に display: block が適用されます。

blazor-error-ui 要素は、blazor-error-ui フォルダー内のサイトのスタイルシートに blazor-error-ui CSS クラスの display: none スタイルが存在するため、通常は非表示になります。 エラーが発生すると、フレームワークによって要素に display: block が適用されます。

詳細な回線エラー

このセクションは、Blazor回線上で動作する Web アプリに適用されます。

このセクションは Blazor Server アプリに適用されます。

クライアント側のエラーには、呼び出し履歴は含まれず、エラーの原因についての詳細は提供されませんが、サーバー ログにはこのような情報が含まれています。 開発目的で、詳細なエラーを有効にすることによって、機密性の高い回線エラー情報をクライアントが利用できるようにすることができます。

CircuitOptions.DetailedErrorstrue に設定します。 詳細と例については、「ASP.NET Core BlazorSignalR のガイダンス」をご覧ください。

CircuitOptions.DetailedErrors を設定する代わりに、アプリの Development 環境設定ファイル (appsettings.Development.json) で DetailedErrors 構成キーを true に設定することもできます。 さらに、SignalR の詳細なログを記録するには、SignalR のサーバー側ログ記録 (Microsoft.AspNetCore.SignalR) を Debug または Trace に設定します。

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

Development/Staging 環境のサーバーまたはローカル システムで、ASPNETCORE_DETAILEDERRORS 環境変数の値を true にすることで、DetailedErrors 構成キーを true に設定することもできます。

警告

インターネット上のクライアントにはエラー情報を常に公開しないようにします。これは、セキュリティ上のリスクです。

Razor コンポーネントのサーバー側レンダリングの詳細なエラー

"このセクションは Blazor WebAssembly に適用されます。"

RazorComponentsServiceOptions.DetailedErrors オプションを使用して、Razor コンポーネントのサーバー側レンダリングのエラーに関する詳細情報の生成を制御します。 既定値は false です。

次の例では、詳細なエラーが有効になっています。

builder.Services.AddRazorComponents(options => 
    options.DetailedErrors = builder.Environment.IsDevelopment());

警告

詳細なエラーは Development 環境でのみ有効にしてください。 詳細なエラーには、悪意のあるユーザーが攻撃に使用できる、アプリに関する機密情報が含まれている可能性があります。

前の例では、IsDevelopment によって返されるの値に基づいて DetailedErrors の値を設定することで、安全性の程度を提供します。 アプリが Development 環境内にある場合は、DetailedErrorstrue に設定されます。 Development 環境内のパブリック サーバー環境で運用アプリをホストできるため、この方法は確実ではありません。

ハンドルされない例外を開発者コードで管理する

エラー後にアプリを続行するには、アプリがエラー処理ロジックを備えている必要があります。 この記事の後のセクションでは、ハンドルされない例外の潜在的原因について説明します。

運用環境では、フレームワークの例外メッセージやスタック トレースを UI に表示しないでください。 例外メッセージやスタック トレースを表示すると以下の可能性があります。

  • エンド ユーザーに機密情報が開示される。
  • 悪意のあるユーザーが、アプリ、サーバー、またはネットワークのセキュリティを侵害する可能性のある脆弱性をアプリの中で発見する助けになる。

回線の未処理例外

このセクションは、回線上で動作するサーバー側アプリに適用されます。

サーバー対話機能が有効になっている Razor コンポーネントはサーバー上でステートフルです。 ユーザーはサーバー上のコンポーネントを操作している間、回線と呼ばれるサーバーへの接続を維持します。 回線では、アクティブなコンポーネント インスタンスに加えて、次のような状態の他の多くの側面が保持されます。

  • コンポーネントの表示される最新の出力。
  • クライアント側のイベントによってトリガーされる可能性がある、イベント処理デリゲートの現在のセット。

ユーザーが複数のブラウザー タブでアプリを開いた場合、ユーザーは複数の独立した回線を作成します。

Blazor は、ハンドルされない例外が発生した回線に対して、ほとんどの例外を致命的として処理します。 ハンドルされない例外のために回線が終了された場合、ユーザーはページを再読み込みして新しい回線を作成するだけで、アプリの操作を続行できます。 他のユーザーや他のブラウザー タブの回線である、終了された回線以外の回線は影響を受けません。 このシナリオは、クラッシュするデスクトップ アプリに似ています。 クラッシュしたアプリを再起動する必要がありますが、他のアプリは影響を受けません。

以下の理由でハンドルされない例外が発生すると、回線はフレームワークによって終了されます。

  • ハンドルされない例外によって、回線が未定義の状態のままになることがよくあります。
  • ハンドルされない例外の後は、アプリの通常動作を保証できません。
  • 回線が未定義状態のままになっていると、アプリにセキュリティの脆弱性が発生するおそれがあります。

グローバル例外処理

グローバル例外処理については、次のセクションを参照してください。

エラー境界

"エラー境界" には、例外を処理するための便利な方法があります。 ErrorBoundary コンポーネント:

  • エラーが発生しなかった場合は、子コンテンツをレンダリングします。
  • ハンドルされない例外がスローされた場合は、エラー UI をレンダリングします。

エラー境界を定義するには、ErrorBoundary コンポーネントを使用して、既存のコンテンツをラップします。 アプリは正常に機能し続けますが、ハンドルされない例外がエラー境界によって処理されます。

<ErrorBoundary>
    ...
</ErrorBoundary>

エラー境界をグローバルな方法で実装するには、アプリのメイン レイアウトの本文コンテンツの周囲に境界を追加できます。

MainLayout.razor:

<article class="content px-4">
    <ErrorBoundary>
        @Body
    </ErrorBoundary>
</article>

エラー境界が静的 MainLayout コンポーネントにのみ適用される Blazor Web アプリでは、境界は静的なサーバー側レンダリング (静的 SSR) フェーズ中にのみアクティブになります。 境界は、コンポーネント階層の下位にあるコンポーネントが対話型であるという理由だけでアクティブになるわけではありません。 MainLayout コンポーネントと、コンポーネント階層の下位にある残りのコンポーネントに対して広範にインタラクティビティを有効にするには、App コンポーネント (Components/App.razor) の HeadOutlet および Routes コンポーネント インスタンスの対話型レンダリングを有効にします。 次の例では、対話型サーバー (InteractiveServer) レンダリング モードを採用しています。

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Routes コンポーネントから、アプリ全体でサーバー インタラクティビティを有効にする必要がない場合は、エラー境界をコンポーネント階層のさらに下に配置します。 たとえば、アプリのメイン レイアウトではなく、インタラクティビティを有効にする個々のコンポーネントのマークアップの周囲にエラー境界を配置します。 留意すべき重要な概念は、エラー境界が配置される場所を問わないということです。

  • エラー境界が対話型ではない場合、静的レンダリング中にサーバー上でのみアクティブ化できます。 たとえば、コンポーネントのライフサイクル メソッドでエラーがスローされたときに、境界がアクティブになります。
  • エラー境界が対話型の場合、ラップされる対話型のサーバー レンダリング コンポーネントに対してアクティブ化できます。

次の例では、カウントが 5 を超えてインクリメントする場合に、Counter コンポーネントによって例外がスローされています。

Counter.razor:

private void IncrementCount()
{
    currentCount++;

    if (currentCount > 5)
    {
        throw new InvalidOperationException("Current count is too big!");
    }
}

5 を超える currentCount に対して、ハンドルされない例外がスローされる場合は、次のようになります。

  • エラーは通常どおりログに記録されます (System.InvalidOperationException: Current count is too big!)。
  • 例外がエラー境界によって処理されます。
  • エラー UI はエラー境界によってレンダリングされ、次のデフォルトのエラー メッセージが表示されます: An error has occurred.

既定では、ErrorBoundary コンポーネントによって、エラー コンテンツの blazor-error-boundary CSS クラスが含まれる空の <div> 要素がレンダリングされます。 既定の UI の色、テキスト、アイコンは、wwwroot フォルダー内にあるアプリのスタイルシートの CSS を使用して定義されるため、エラー UI を自由にカスタマイズすることができます。

ErrorContent プロパティを設定して、既定のエラー コンテンツを変更します。

<ErrorBoundary>
    <ChildContent>
        @Body
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
    </ErrorContent>
</ErrorBoundary>

エラー境界は前の例のレイアウトで定義されているので、エラー発生後にユーザーがどのページに移動したかにかかわらず、エラー UI が表示されます。 ほとんどのシナリオで、エラー境界の範囲を狭くすることをお勧めします。 エラー境界の範囲を広く設定する場合は、エラー境界の Recover メソッドを呼び出すことによって、後続のページ ナビゲーション イベントでエラー以外の状態にリセットできます。

MainLayout.razorの場合:

...

<ErrorBoundary @ref="errorBoundary">
    @Body
</ErrorBoundary>

...

@code {
    private ErrorBoundary? errorBoundary;

    protected override void OnParametersSet()
    {
        errorBoundary?.Recover();
    }
}

回復するとエラーが再度スローされるコンポーネントが再レンダリングされるだけの無限ループを避けるために、レンダリング ロジックから Recover を呼び出さないでください。 次の場合にのみ Recover を呼び出します。

  • ユーザーは、ボタンの選択などの UI ジェスチャを実行して、手順を再試行するか、新しいコンポーネントに移動することを示します。
  • 追加のロジックによっても例外がクリアされます。 コンポーネントが再レンダリングされると、エラーは再発しません。

代替のグローバル例外処理

エラー境界 (ErrorBoundary) を使用する代わりに、カスタム エラー コンポーネントを CascadingValue として子コンポーネントに渡すこともできます。 挿入されたサービスまたはカスタム ロガーの実装を使用するよりもコンポーネントを使用する利点は、カスケードされたコンポーネントがコンテンツをレンダリングし、エラーが発生したときに CSS スタイルを適用できることです。

次の Error コンポーネントの例では、単にエラーをログするだけですが、コンポーネントのメソッドは、アプリが必要とする任意の方法で (複数のエラー処理メソッドを使用するなど) エラーを処理できます。

Error.razor:

@inject ILogger<Error> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);

        // Call StateHasChanged if ProcessError directly participates in 
        // rendering. If ProcessError only logs or records the error,
        // there's no need to call StateHasChanged.
        //StateHasChanged();
    }
}

注意

RenderFragment の詳細については、ASP.NET Core Razor コンポーネントに関する記事を参照してください。

Routes コンポーネントで、Router コンポーネント (<Router>...</Router>) を Error コンポーネントでラップします。 これにより、Error コンポーネントは、Error コンポーネントが CascadingParameter として受信されるアプリの任意のコンポーネントにカスケードできるようになります。

Routes.razor:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

App コンポーネントで、Router コンポーネント (<Router>...</Router>) を Error コンポーネントでラップします。 これにより、Error コンポーネントは、Error コンポーネントが CascadingParameter として受信されるアプリの任意のコンポーネントにカスケードできるようになります。

App.razorの場合:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

コンポーネントのエラーを処理するには:

  • Error コンポーネントを @code ブロック内の CascadingParameter として指定します。 Blazor プロジェクト テンプレートに基づくアプリの Counter コンポーネントの例で、次の Error プロパティを追加します。

    [CascadingParameter]
    public Error? Error { get; set; }
    
  • 適切な例外の種類を使用して、任意の catch ブロックでエラー処理メソッドを呼び出します。 この例の Error コンポーネントは 1 つの ProcessError メソッドのみを提供しますが、エラー処理コンポーネントは、アプリ全体の他のエラー処理要件に対処するために、任意の数のエラー処理メソッドを提供できます。 次の Counter コンポーネントの例では、カウントが 5 より大きい場合に例外がスローされ、トラップされます。

    @code {
        private int currentCount = 0;
    
        [CascadingParameter]
        public Error? Error { get; set; }
    
        private void IncrementCount()
        {
            try
            {
                currentCount++;
    
                if (currentCount > 5)
                {
                    throw new InvalidOperationException("Current count is over five!");
                }
            }
            catch (Exception ex)
            {
                Error?.ProcessError(ex);
            }
        }
    }
    

前の Error コンポーネントと Counter コンポーネントに対して行われた前の変更を共に使用すると、ブラウザーの開発者ツール コンソールに、トラップされ、ログされた次のエラーが示されます。

fail: {COMPONENT NAMESPACE}.Error[0]
Error:ProcessError - Type: System.InvalidOperationException Message: Current count is over five!

カスタム エラー メッセージ バーの表示やレンダリングされた要素の CSS スタイルの変更など、ProcessError メソッドがレンダリングに直接関与している場合は、ProcessErrors メソッドの最後で StateHasChanged を呼び出して、UI を再レンダリングします。

このセクションの方法は、try-catch ステートメントを使用してエラーを処理するものなので、エラーが発生しても回線が生きていると、クライアントとサーバーの間のアプリの SignalR 接続が切断されることはありません。 その他のハンドルされない例外は、回線にとって致命的なままです。 詳細については、未処理の例外に対する回線の反応に関するセクションを参照してください。

アプリはエラー処理コンポーネントをカスケード値として使用して、一元的な方法でエラーを処理できます。

次の Error コンポーネントは、それ自体を CascadingValue として子コンポーネントに渡します。 次の例では、単にエラーをログするだけですが、コンポーネントのメソッドは、アプリが必要とする任意の方法で (複数のエラー処理メソッドを使用するなど) エラーを処理できます。 挿入されたサービスまたはカスタム ロガーの実装を使用するよりもコンポーネントを使用する利点は、カスケードされたコンポーネントがコンテンツをレンダリングし、エラーが発生したときに CSS スタイルを適用できることです。

Error.razor:

@using Microsoft.Extensions.Logging
@inject ILogger<Error> Logger

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);
    }
}

注意

RenderFragment の詳細については、ASP.NET Core Razor コンポーネントに関する記事を参照してください。

App コンポーネントで Router コンポーネントを Error コンポーネントでラップします。 これにより、Error コンポーネントは、Error コンポーネントが CascadingParameter として受信されるアプリの任意のコンポーネントにカスケードできるようになります。

App.razor:

<Error>
    <Router ...>
        ...
    </Router>
</Error>

コンポーネントのエラーを処理するには:

  • Error コンポーネントを @code ブロック内の CascadingParameter として指定します。

    [CascadingParameter]
    public Error Error { get; set; }
    
  • 適切な例外の種類を使用して、任意の catch ブロックでエラー処理メソッドを呼び出します。 この例の Error コンポーネントは 1 つの ProcessError メソッドのみを提供しますが、エラー処理コンポーネントは、アプリ全体の他のエラー処理要件に対処するために、任意の数のエラー処理メソッドを提供できます。

    try
    {
        ...
    }
    catch (Exception ex)
    {
        Error.ProcessError(ex);
    }
    

前の例の Error コンポーネントと ProcessError メソッドを使用すると、ブラウザーの開発者ツール コンソールに、トラップされログされた次のエラーが示されます。

失敗: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException メッセージ: オブジェクト参照がオブジェクトのインスタンスに設定されていません。

カスタム エラー メッセージ バーの表示やレンダリングされた要素の CSS スタイルの変更など、ProcessError メソッドがレンダリングに直接関与している場合は、ProcessErrors メソッドの最後で StateHasChanged を呼び出して、UI を再レンダリングします。

このセクションの方法は、try-catch ステートメントを使用してエラーを処理するものなので、エラーが発生しても回線が生きていると、クライアントとサーバーの間の Blazor アプリの SignalR 接続が切断されることはありません。 すべてのハンドルされない例外は回線にとって致命的です。 詳細については、未処理の例外に対する回線の反応に関するセクションを参照してください。

永続的プロバイダーを使用してエラーをログに記録する

ハンドルされない例外が発生した場合、例外は、サービス コンテナー内に構成されている ILogger インスタンスにログ記録されます。 既定では、Blazor アプリは、コンソール ログ プロバイダーを使用してコンソール出力にログを記録します。 ログ サイズとログ ローテーションを管理するプロバイダーを使用して、サーバー上の場所 (またはクライアント側アプリのバックエンド Web API) にログすることを検討してください。 または、Azure Application Insights (Azure Monitor) などのアプリケーション パフォーマンス管理 (APM) サービスを、アプリで使用することもできます。

メモ

アプリをサポートする Application Insights のネイティブ機能と、Google Analytics に対する Blazor フレームワークのネイティブ サポートは、これらのテクノロジの今後のリリースで利用できるようになる可能性があります。 詳細については、「Blazor WASM クライアント側での App Insights のサポート (microsoft/ApplicationInsights-dotnet #2143)」および「Web 分析と診断 (dotnet/aspnetcore #5461)」 (コミュニティ実装へのリンクを含む) 参照してください。 それまでの間、クライアント側のアプリでは、Application Insights JavaScript SDKJS相互運用を使用して、クライアント側アプリから Application Insights にエラーを直接記録できます。

回線上で動作する Blazor アプリの開発中、アプリは通常、デバッグを支援するために例外の完全な詳細をブラウザーのコンソールに送信します。 運用時には、詳細なエラーはクライアントに送信されませんが、例外の詳細がサーバーに記録されます。

ログに記録するインシデントと、ログに記録されるインシデントの重大度レベルを決定する必要があります。 悪意のあるユーザーが、意図的にエラーをトリガーできる可能性もあります。 たとえば、製品の詳細を表示するコンポーネントの URL に不明な ProductId が指定されているエラーのインシデントは、ログに記録しないようにします。 すべてのエラーをログ記録の対象となるインシデントとして扱うことは避けてください。

詳細については、次の記事を参照してください。

‡サーバー側 Blazor アプリと、Blazor 用の Web API バックエンド アプリである他のサーバー側 ASP.NET Core アプリに適用されます。 クライアント側アプリによってクライアント側のエラー情報をトラップして Web API に送信できます。そこで、エラー情報が永続的なログ プロバイダーにログされます。

ハンドルされない例外が発生した場合、例外は、サービス コンテナー内に構成されている ILogger インスタンスにログ記録されます。 既定では、Blazor アプリは、コンソール ログ プロバイダーを使用してコンソール出力にログを記録します。 ログ サイズ管理とログ ローテーションを備えたログ プロバイダーを使用するバックエンド Web API にエラー情報を送信することにより、サーバー上のより永続的な場所にログを記録することを検討してください。 または、バックエンド Web API アプリにより、Azure Application Insights (Azure Monitor)† などのアプリケーション パフォーマンス管理 (APM) サービスを使用して、クライアントから受信したエラー情報を記録することもできます。

ログに記録するインシデントと、ログに記録されるインシデントの重大度レベルを決定する必要があります。 悪意のあるユーザーが、意図的にエラーをトリガーできる可能性もあります。 たとえば、製品の詳細を表示するコンポーネントの URL に不明な ProductId が指定されているエラーのインシデントは、ログに記録しないようにします。 すべてのエラーをログ記録の対象となるインシデントとして扱うことは避けてください。

詳細については、次の記事を参照してください。

†アプリをサポートする Application Insights のネイティブ機能と、Google Analytics に対する Blazor フレームワークのネイティブ サポートは、これらのテクノロジの今後のリリースで利用できるようになる可能性があります。 詳細については、「Blazor WASM クライアント側での App Insights のサポート (microsoft/ApplicationInsights-dotnet #2143)」および「Web 分析と診断 (dotnet/aspnetcore #5461)」 (コミュニティ実装へのリンクを含む) 参照してください。 それまでの間、クライアント側のアプリでは、Application Insights JavaScript SDKJS相互運用を使用して、クライアント側アプリから Application Insights にエラーを直接記録できます。

‡Blazor アプリ用の Web API バックエンド アプリであるサーバー側 ASP.NET Core アプリに適用されます。 クライアント側アプリによってエラー情報をトラップして Web API に送信します。そこで、エラー情報が永続的なログプロバイダーに記録されます。

エラーが発生する可能性のある場所

次のいずれかの場所で、フレームワークとアプリのコードにより、ハンドルされない例外がトリガーされる場合があります。詳細については、この記事の以降のセクションで説明します。

コンポーネントのインスタンス化

Blazor によってコンポーネントのインスタンスが作成されるとき:

  • コンポーネントのコンストラクターが呼び出されます。
  • @inject ディレクティブ、または [Inject] 属性を介して、コンポーネントのコンストラクターに渡される DI サービスのコンストラクターが呼び出されます。

実行されたコンストラクターまたは任意の [Inject] プロパティのセッターでエラーが発生すると、ハンドルされない例外になり、フレームワークによるコンポーネントのインスタンス化が停止されます。 アプリが回線経由で動作している場合、回線に障害が発生します。 コンストラクターのロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップする必要があります。

ライフサイクル メソッド

コンポーネントの有効期間の間は、Blazor によってライフサイクル メソッドが呼び出されます。 いずれかのライフサイクル メソッドが同期的または非同期的に例外をスローした場合、例外は 回線にとって致命的です。 コンポーネントでライフサイクル メソッドのエラーに対処するには、エラー処理ロジックを追加します。

OnParametersSetAsync によって製品を取得するメソッドを呼び出す次の例では:

  • ProductRepository.GetProductByIdAsync メソッドでスローされた例外は try-catch ステートメントによって処理されます。
  • catch ブロックの実行時には:
    • loadFailedtrue に設定されます。これがユーザーにエラー メッセージを表示するために使われます。
    • エラーがログに記録されます。
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product

<PageTitle>Product Details</PageTitle>

<h1>Product Details Example</h1>

@if (details != null)
{
    <h2>@details.ProductName</h2>
    <p>
        @details.Description
        <a href="@details.Url">Company Link</a>
    </p>
    
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await Product.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
        public string? Url { get; set; }
    }

    /*
    * Register the service in Program.cs:
    * using static BlazorSample.Components.Pages.ProductDetails;
    * builder.Services.AddScoped<IProductRepository, ProductRepository>();
    */

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }

    public class ProductRepository : IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id)
        {
            return Task.FromResult(
                new ProductDetail()
                {
                    ProductName = "Flowbee ",
                    Description = "The Revolutionary Haircutting System You've Come to Love!",
                    Url = "https://flowbee.com/"
                });
        }
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail? details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string? ProductName { get; set; }
        public string? Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository

@if (details != null)
{
    <h1>@details.ProductName</h1>
    <p>@details.Description</p>
}
else if (loadFailed)
{
    <h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
    <h1>Loading...</h1>
}

@code {
    private ProductDetail details;
    private bool loadFailed;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            loadFailed = false;

            // Reset details to null to display the loading indicator
            details = null;

            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

    public class ProductDetail
    {
        public string ProductName { get; set; }
        public string Description { get; set; }
    }

    public interface IProductRepository
    {
        public Task<ProductDetail> GetProductByIdAsync(int id);
    }
}

レンダリング ロジック

Razor コンポーネント ファイル (.razor) 内の宣言マークアップは、BuildRenderTree という名前の C# メソッドにコンパイルされます。 コンポーネントがレンダリングされるときには、BuildRenderTree が実行されて、レンダリングされたコンポーネントの要素、テキスト、および子コンポーネントを記述するデータ構造が構築されます。

レンダリング ロジックは例外をスローすることがあります。 このシナリオの例は、@someObject.PropertyName が評価されても @someObjectnull であるときに発生しています。 回線上で動作する Blazor アプリの場合、レンダリング ロジックによってスローされる未処理の例外は、アプリの回線にとって致命的です。

レンダリング ロジックで NullReferenceException が発生しないようにするには、そのメンバーにアクセスする前に null オブジェクトかどうかを調べます。 次の例では、person.Addressnull の場合は person.Address プロパティにアクセスしません。

@if (person.Address != null)
{
    <div>@person.Address.Line1</div>
    <div>@person.Address.Line2</div>
    <div>@person.Address.City</div>
    <div>@person.Address.Country</div>
}

上記のコードは、personnull でないことを前提としています。 多くの場合、コードの構造によって、コンポーネントがレンダリングされる時点でオブジェクトの存在が保証されます。 そのような場合は、レンダリング ロジックで null かどうかを調べる必要はありません。 前の例では、コンポーネントがインスタンス化されるときに person が作成されるため、person が存在することが保証されている可能性があります。次にその例を示します。

@code {
    private Person person = new();

    ...
}

イベント ハンドラー

クライアント側のコードでは、以下を使用して、イベント ハンドラーが作成されるときに C# コードの呼び出しをトリガーします。

  • @onclick
  • @onchange
  • その他の @on... 属性
  • @bind

これらのシナリオでは、イベント ハンドラー コードによって、ハンドルされない例外がスローされることがあります。

アプリが外部の理由で失敗する可能性のあるコードを呼び出す場合は、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップします。

イベント ハンドラーが、開発者コードによってトラップおよび処理されない、ハンドルされない例外をスローした場合 (たとえば、データベース クエリが失敗した):

  • フレームワークによって例外がログされます。
  • 回線を介して動作する Blazor アプリでは、例外はアプリの回線にとって致命的です。

コンポーネントの廃棄

たとえばユーザーが別のページに移動したため、コンポーネントが UI から削除されることがあります。 System.IDisposable を実装しているコンポーネントが UI から削除されると、フレームワークにより、コンポーネントの Dispose メソッドが呼び出されます。

コンポーネントの Dispose メソッドが、回路上で動作する Blazor アプリで未処理の例外をスローした場合、その例外はアプリの回路にとって致命的です。

破棄のロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップする必要があります。

コンポーネントの破棄について詳しくは、「ASP.NET Core Razor コンポーネントのライフサイクル」をご覧ください。

JavaScript 相互運用

IJSRuntime は Blazor フレームワークによって登録されます。 IJSRuntime.InvokeAsync を使用すると、.NET コードによって、ユーザーのブラウザーで JavaScript (JS) ランタイムの非同期呼び出しを行えます。

InvokeAsync を使用するエラー処理には、以下の条件が適用されます。

  • InvokeAsync の呼び出しが同期的に失敗した場合は、.NET 例外が発生します。 たとえば、指定された引数をシリアル化できないため、InvokeAsync の呼び出しが失敗することがあります。 開発者コードで例外をキャッチする必要があります。 イベント ハンドラーまたはコンポーネントのライフサイクル メソッドのアプリ コードが、回線上で動作する Blazor アプリの例外を処理しない場合、結果として生じる例外はアプリの回線にとって致命的です。
  • InvokeAsync の呼び出しが非同期に失敗した場合、.NET Task が失敗します。 たとえば、JS 側のコードが例外をスローしたり、rejected として完了した Promise を返したりするために、InvokeAsync の呼び出しが失敗することがあります。 開発者コードで例外をキャッチする必要があります。 await 演算子を使用する場合は、エラー処理とログ記録を含む try-catch ステートメントでメソッド呼び出しをラップすることを検討してください。 そうしないと、回線上で動作する Blazor アプリでコードに障害が発生すると、アプリの回線にとって致命的な未処理の例外が発生します。
  • 既定では、InvokeAsync の呼び出しは一定の期間内に完了する必要があります。そうでないと呼び出しがタイムアウトになります。 タイムアウトにより、完了メッセージを送り返さないネットワーク接続や JS コードでの損失からコードを保護します。 呼び出しがタイムアウトになった場合、結果の System.Threading.TasksOperationCanceledException で失敗します。 ログ記録を使用して例外をトラップし、処理します。

同様に、JS コードを使用して、[JSInvokable] 属性によって示される .NET メソッドの呼び出しを開始できます。 ハンドルされない例外が、これらの .NET メソッドでスローされた場合:

  • 回線を介して動作する Blazor アプリでは、例外はアプリの回線にとって致命的なものとして扱われません
  • JS 側の Promise は拒否されます。

.NET 側か、メソッド呼び出しの JS 側か、どちらでエラー処理コードを使用するかを選択できます。

詳細については、次の記事を参照してください。

プリレンダリング

Razor コンポーネントは既定で事前レンダリングされるため、レンダリングされた HTML マークアップはユーザーの最初の HTTP 要求の一部として返されます。

回線上で動作する Blazor アプリでは、プリレンダリングは次のように機能します。

  • 同じページに含まれるすべての事前レンダリング コンポーネントに対する新しい回線を作成する。
  • 初期 HTML を生成する。
  • ユーザーのブラウザーが同じサーバーに戻る SignalR 接続を確立するまで、回線を disconnected として扱う。 接続が確立されると、回線でのインタラクティビティが再開され、コンポーネントの HTML マークアップが更新されます。

事前レンダリングされたクライアント側コンポーネントの場合、事前レンダリングは次のように機能します。

  • 同じページに含まれるプリレンダリング済みコンポーネントすべてについて、サーバー上で最初の HTML を生成します。
  • ブラウザーがアプリのコンパイル済みコードと .NET ランタイム (まだ読み込んでいない場合) をバックグラウンドで読み込んだ後、クライアントでコンポーネントを対話型にします。

ライフサイクル メソッドやレンダリング ロジックの実行中など、プリレンダリング中にコンポーネントからハンドルされない例外がスローされた場合:

  • 回線を介して動作する Blazor アプリでは、例外は回線にとって致命的です。 事前にレンダリングされたクライアント側コンポーネントの場合、例外によりコンポーネントのレンダリングが妨げられます。
  • その例外は、ComponentTagHelper の呼び出し履歴から破棄されます。

事前レンダリングが失敗する通常の状況では、コンポーネントのビルドとレンダリングを続行しても意味がありません。これは、動作中のコンポーネントはレンダリングできないためです。

事前レンダリング中に発生する可能性のあるエラーに耐えるには、例外をスローする可能性のあるコンポーネント内にエラー処理ロジックを配置する必要があります。 エラー処理とログ記録を含む try-catch ステートメントを使用してください。 try-catch ステートメント内に ComponentTagHelper をラップするのではなく、ComponentTagHelper によってレンダリングされるコンポーネント内にエラー処理ロジックを配置します。

高度なシナリオ

再帰的レンダリング

コンポーネントは、再帰的に入れ子にすることができます。 これは、再帰的なデータ構造を表現する場合に役立ちます。 たとえば TreeNode コンポーネントで、ノードの子ごとにより多くの TreeNode コンポーネントをレンダリングできます。

再帰的にレンダリングする場合は、無限の再帰となるようなコーディング パターンは回避します。

  • 循環が含まれるデータ構造は再帰的にレンダリングしないでください。 たとえば、子にそれ自体が含まれるツリー ノードはレンダリングしないでください。
  • 循環を含むひと続きのレイアウトは作成しないでください。 たとえば、レイアウトがそれ自体であるレイアウトは作成しないようにします。
  • エンドユーザーが、悪意のあるデータ入力や JavaScript の相互運用呼び出しを通して、再帰による不変 (ルール) を犯さないようにします。

レンダリング中の無限ループ:

  • レンダリング プロセスが永久に続行されるようになります。
  • これは終了しないループを作成するのと同じです。

これらのシナリオでは、Blazor は失敗し、通常は次のことを試行します。

  • オペレーティング システムで許されている限りの CPU 時間を無期限に消費します。
  • メモリの量を無制限に消費します。 メモリを無制限に使用することは、すべての反復処理で、終了しないループによってコレクションにエントリが追加されるシナリオと同じです。

無限の再帰パターンを回避するには、再帰的なレンダリング コードに適切な停止条件が含まれるようにします。

カスタム レンダリング ツリーのロジック

ほとんどの Razor コンポーネントは、Razor コンポーネント ファイル (.razor) として実装され、RenderTreeBuilder 上で動作して出力をレンダリングするロジックを生成するため、フレームワークによってコンパイルされます。 ただし、開発者は、手続き型の C# コードを使用して RenderTreeBuilder ロジックを手動で実装できます。 詳細については、「ASP.NET Core Blazor の高度なシナリオ (レンダー ツリーの構築)」を参照してください。

警告

手動のレンダー ツリー ビルダー ロジックの使用は、高度で安全ではないシナリオと考えられています。一般のコンポーネント開発には推奨されません。

RenderTreeBuilder コードが記述される場合は、開発者がコードの正確性を保証する必要があります。 たとえば、開発者は以下のことを確認する必要があります。

  • OpenElementCloseElement の呼び出しが正しく調整されている。
  • 正しい場所にのみ属性が追加される。

手動レンダー ツリー ビルダーのロジックが正しくないと、クラッシュ、アプリまたはサーバーのハング、セキュリティ脆弱性など、不特定の未定義の動作が発生するおそれがあります。

手動のレンダリング ツリー ビルダー ロジックは、アセンブリ コードや Microsoft Intermediate Language (MSIL) 命令を手動で記述するのと同じレベルの複雑さと、同じレベルの "危険性" を伴うことを考慮に入れてください。

その他のリソース

† クライアント側 Blazor アプリでログ記録に使用されるバックエンド ASP.NET Core Web API アプリに適用されます。