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

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

開発中の詳細なエラー

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

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

このエラー処理エクスペリエンスの UI は、Blazor プロジェクト テンプレートの一部です。

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

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

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

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

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

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

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

Blazor is a single-page application (SPA) client-side framework. The browser serves as the app's host and thus acts as the processing pipeline for individual Razor components based on URI requests for navigation and static assets. Unlike ASP.NET Core apps that run on the server with a middleware processing pipeline, there is no middleware pipeline that processes requests for Razor components that can be leveraged for global error handling. However, an app can use an error processing component as a cascading value to process errors in a centralized way.

The following Error component passes itself as a CascadingValue to child components. The following example merely logs the error, but methods of the component can process errors in any way required by the app, including through the use of multiple error processing methods. An advantage of using a component over using an injected service or a custom logger implementation is that a cascaded component can render content and apply CSS styles when an error occurs.

Shared/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);
    }
}

In the App component, wrap the Router component with the Error component. This permits the Error component to cascade down to any component of the app where the Error component is received as a CascadingParameter.

App.razor:

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

To process errors in a component:

  • Designate the Error component as a CascadingParameter in the @code block:

    [CascadingParameter]
    public Error Error { get; set; }
    
  • Call an error processing method in any catch block with an appropriate exception type. The example Error component only offers a single ProcessError method, but the error processing component can provide any number of error processing methods to address alternative error processing requirements throughout the app.

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

Using the preceding example Error component and ProcessError method, the browser's developer tools console indicates the trapped, logged error:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: Object reference not set to an instance of an object.

If the ProcessError method directly participates in rendering, such as showing a custom error message bar or changing the CSS styles of the rendered elements, call StateHasChanged at the end of the ProcessErrors method to rerender the UI.

Error boundaries provide a convenient approach for handling exceptions. The ErrorBoundary component:

  • Renders its child content when an error hasn't occurred.
  • Renders error UI when an unhandled exception is thrown.

To define an error boundary, use the ErrorBoundary component to wrap existing content. For example, an error boundary can be added around the body content of the app's main layout.

Shared/MainLayout.razor:

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

The app continues to function normally, but the error boundary handles unhandled exceptions.

Consider the following example, where the Counter component throws an exception if the count increments past five.

In Pages/Counter.razor:

private void IncrementCount()
{
    currentCount++;

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

If the unhandled exception is thrown for a currentCount over five:

  • The exception is handled by the error boundary.
  • Error UI is rendered (An error has occurred!).

By default, the ErrorBoundary component renders an empty <div> element with the blazor-error-boundary CSS class for its error content. The colors, text, and icon for the default UI are defined using CSS in the app's stylesheet in the wwwroot folder, so you're free to customize the error UI.

You can also change the default error content by setting the ErrorContent property:

<ErrorBoundary>
    <ChildContent>
        @Body
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">Nothing to see here right now. Sorry!</p>
    </ErrorContent>
</ErrorBoundary>

Because the error boundary is defined in the layout in the preceding examples, the error UI is seen regardless of which page the user navigated to. We recommend narrowly scoping error boundaries in most scenarios. If you do broadly scope an error boundary, you can reset it to a non-error state on subsequent page navigation events by calling the error boundary's Recover method:

...

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

...

@code {
    private ErrorBoundary errorBoundary;

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

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

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

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

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

† Blazor WebAssembly アプリをサポートする Application Insights のネイティブ機能と、Google Analytics に対する Blazor フレームワークのネイティブ サポートは、これらのテクノロジの今後のリリースで利用できるようになる可能性があります。 詳細については、「Blazor WASM クライアント側での App Insights のサポート (microsoft/ApplicationInsights-dotnet #2143)」および「Web 分析と診断 (dotnet/aspnetcore #5461)」 (コミュニティ実装へのリンクを含む) 参照してください。 それまでの間、クライアント側の Blazor WebAssembly アプリでは、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}"
@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;
            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

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

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

レンダリング ロジック

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

レンダリング ロジックは例外をスローすることがあります。 このシナリオの例は、@someObject.PropertyName が評価されても @someObjectnull であるときに発生しています。

レンダリング ロジックで 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 ステートメントを使用して、例外をトラップします。

ユーザー コードで例外のトラップと処理が行われない場合は、フレームワークによって例外がログに記録されます。

コンポーネントの廃棄

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

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

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

JavaScript 相互運用

IJSRuntime.InvokeAsync を使用すると、.NET コードによって、ユーザーのブラウザーで JavaScript ランタイムの非同期呼び出しを行えます。

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

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

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

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

これらのシナリオでは、通常、スレッドによって以下のことが試みられます。

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

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

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

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

警告

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

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

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

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

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

その他のリソース

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

開発中の詳細なエラー

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

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

このエラー処理エクスペリエンスの UI は、Blazor プロジェクト テンプレートの一部です。

Blazor Server アプリでは、Pages/_Layout.cshtml ファイルでエクスペリエンスをカスタマイズします。

<div id="blazor-error-ui">
    <environment include="Staging,Production">
        An error has occurred. This application may no longer respond until reloaded.
    </environment>
    <environment include="Development">
        An unhandled exception has occurred. See browser dev tools for details.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

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

Blazor Server の詳細な回線エラー

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

CircuitOptions.DetailedErrorstrue に設定します。 使用例を含む詳細については、「ASP.NET Core Blazor SignalR ガイダンス」を参照してください。

CircuitOptions.DetailedErrors を設定する代わりに、アプリの開発環境設定ファイル (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"
    }
  }
}

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

警告

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

ハンドルされない例外に対して Blazor Server アプリがどのように反応するか

Blazor Server はステートフルなフレームワークです。 ユーザーはアプリを操作するときに、回線 と呼ばれる、サーバーへの接続を維持します。 回線では、アクティブなコンポーネント インスタンスに加えて、次のような状態の他の多くの側面が保持されます。

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

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

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

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

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

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

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

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

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

Blazor is a single-page application (SPA) client-side framework. The browser serves as the app's host and thus acts as the processing pipeline for individual Razor components based on URI requests for navigation and static assets. Unlike ASP.NET Core apps that run on the server with a middleware processing pipeline, there is no middleware pipeline that processes requests for Razor components that can be leveraged for global error handling. However, an app can use an error processing component as a cascading value to process errors in a centralized way.

The following Error component passes itself as a CascadingValue to child components. The following example merely logs the error, but methods of the component can process errors in any way required by the app, including through the use of multiple error processing methods. An advantage of using a component over using an injected service or a custom logger implementation is that a cascaded component can render content and apply CSS styles when an error occurs.

Shared/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);
    }
}

In the App component, wrap the Router component with the Error component. This permits the Error component to cascade down to any component of the app where the Error component is received as a CascadingParameter.

App.razor:

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

To process errors in a component:

  • Designate the Error component as a CascadingParameter in the @code block:

    [CascadingParameter]
    public Error Error { get; set; }
    
  • Call an error processing method in any catch block with an appropriate exception type. The example Error component only offers a single ProcessError method, but the error processing component can provide any number of error processing methods to address alternative error processing requirements throughout the app.

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

Using the preceding example Error component and ProcessError method, the browser's developer tools console indicates the trapped, logged error:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: Object reference not set to an instance of an object.

If the ProcessError method directly participates in rendering, such as showing a custom error message bar or changing the CSS styles of the rendered elements, call StateHasChanged at the end of the ProcessErrors method to rerender the UI.

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

Error boundaries provide a convenient approach for handling exceptions. The ErrorBoundary component:

  • Renders its child content when an error hasn't occurred.
  • Renders error UI when an unhandled exception is thrown.

To define an error boundary, use the ErrorBoundary component to wrap existing content. For example, an error boundary can be added around the body content of the app's main layout.

Shared/MainLayout.razor:

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

The app continues to function normally, but the error boundary handles unhandled exceptions.

Consider the following example, where the Counter component throws an exception if the count increments past five.

In Pages/Counter.razor:

private void IncrementCount()
{
    currentCount++;

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

If the unhandled exception is thrown for a currentCount over five:

  • The exception is handled by the error boundary.
  • Error UI is rendered (An error has occurred!).

By default, the ErrorBoundary component renders an empty <div> element with the blazor-error-boundary CSS class for its error content. The colors, text, and icon for the default UI are defined using CSS in the app's stylesheet in the wwwroot folder, so you're free to customize the error UI.

You can also change the default error content by setting the ErrorContent property:

<ErrorBoundary>
    <ChildContent>
        @Body
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">Nothing to see here right now. Sorry!</p>
    </ErrorContent>
</ErrorBoundary>

Because the error boundary is defined in the layout in the preceding examples, the error UI is seen regardless of which page the user navigated to. We recommend narrowly scoping error boundaries in most scenarios. If you do broadly scope an error boundary, you can reset it to a non-error state on subsequent page navigation events by calling the error boundary's Recover method:

...

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

...

@code {
    private ErrorBoundary errorBoundary;

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

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

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

通常、開発中は、Blazor Server アプリによって、デバッグの助けになるよう、例外の完全な詳細情報がブラウザーのコンソールに送信されます。 運用時には、詳細なエラーはクライアントに送信されませんが、例外の詳細がサーバーに記録されます。

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

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

† Blazor アプリ用の Web API バックエンド アプリであるサーバー側 ASP.NET Core アプリに適用されます。

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

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

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

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

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

実行されたいずれかのコンストラクターまたは任意の [Inject] プロパティのセッターがハンドルされない例外をスローすると、Blazor Server の回線が失敗します。 フレームワークではコンポーネントをインスタンス化できないため、この例外は致命的です。 コンストラクターのロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップする必要があります。

ライフサイクル メソッド

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

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

  • ProductRepository.GetProductByIdAsync メソッドでスローされた例外は try-catch ステートメントによって処理されます。
  • catch ブロックの実行時には:
    • loadFailedtrue に設定されます。これがユーザーにエラー メッセージを表示するために使われます。
    • エラーがログに記録されます。
@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;
            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

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

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

レンダリング ロジック

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

レンダリング ロジックは例外をスローすることがあります。 このシナリオの例は、@someObject.PropertyName が評価されても @someObjectnull であるときに発生しています。 レンダリング ロジックによってスローされるハンドルされない例外は、Blazor Server 回線にとって致命的です。

レンダリング ロジックで 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

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

イベント ハンドラーがハンドルされない例外をスローした場合 (たとえば、データベース クエリが失敗した)、例外は Blazor Server 回線にとって致命的です。 アプリが外部の理由で失敗する可能性のあるコードを呼び出す場合は、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップします。

ユーザー コードで例外のトラップと処理が行われない場合は、フレームワークによって例外がログに記録され、回線が終了されます。

コンポーネントの廃棄

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

コンポーネントの Dispose メソッドがハンドルされない例外をスローした場合、例外は Blazor Server 回線にとって致命的です。 破棄のロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップする必要があります。

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

JavaScript 相互運用

IJSRuntime.InvokeAsync を使用すると、.NET コードによって、ユーザーのブラウザーで JavaScript ランタイムの非同期呼び出しを行えます。

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

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

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

  • 例外は Blazor Server 回線にとって致命的なものとして扱われません。
  • JavaScript 側の Promise が拒否されます。

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

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

Blazor Server のプリレンダリング

レンダリングされた HTML マークアップがユーザーの初期 HTTP 要求の一部として返されるように、コンポーネント タグ ヘルパーを使用して Blazor コンポーネントを事前レンダリングすることができます。 これは以下によって機能します。

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

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

  • 例外は回線にとって致命的です。
  • その例外は、ComponentTagHelper タグ ヘルパーの呼び出し履歴から破棄されます。 そのため、開発者コードによって例外が明示的にキャッチされない限り、HTTP 要求全体が失敗します。

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

これらのシナリオでは、影響を受けた Blazor Server 回線が失敗し、スレッドでは通常、以下のことが試みられます。

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

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

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

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

警告

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

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

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

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

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

その他のリソース

† Blazor アプリ用の Web API バックエンド アプリであるサーバー側 ASP.NET Core アプリに適用されます。

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

開発中の詳細なエラー

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

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

このエラー処理エクスペリエンスの UI は、Blazor プロジェクト テンプレートの一部です。

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

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

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

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

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

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

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

Blazor is a single-page application (SPA) client-side framework. The browser serves as the app's host and thus acts as the processing pipeline for individual Razor components based on URI requests for navigation and static assets. Unlike ASP.NET Core apps that run on the server with a middleware processing pipeline, there is no middleware pipeline that processes requests for Razor components that can be leveraged for global error handling. However, an app can use an error processing component as a cascading value to process errors in a centralized way.

The following Error component passes itself as a CascadingValue to child components. The following example merely logs the error, but methods of the component can process errors in any way required by the app, including through the use of multiple error processing methods. An advantage of using a component over using an injected service or a custom logger implementation is that a cascaded component can render content and apply CSS styles when an error occurs.

Shared/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);
    }
}

In the App component, wrap the Router component with the Error component. This permits the Error component to cascade down to any component of the app where the Error component is received as a CascadingParameter.

App.razor:

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

To process errors in a component:

  • Designate the Error component as a CascadingParameter in the @code block:

    [CascadingParameter]
    public Error Error { get; set; }
    
  • Call an error processing method in any catch block with an appropriate exception type. The example Error component only offers a single ProcessError method, but the error processing component can provide any number of error processing methods to address alternative error processing requirements throughout the app.

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

Using the preceding example Error component and ProcessError method, the browser's developer tools console indicates the trapped, logged error:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: Object reference not set to an instance of an object.

If the ProcessError method directly participates in rendering, such as showing a custom error message bar or changing the CSS styles of the rendered elements, call StateHasChanged at the end of the ProcessErrors method to rerender the UI.

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

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

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

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

† Blazor WebAssembly アプリをサポートする Application Insights のネイティブ機能と、Google Analytics に対する Blazor フレームワークのネイティブ サポートは、これらのテクノロジの今後のリリースで利用できるようになる可能性があります。 詳細については、「Blazor WASM クライアント側での App Insights のサポート (microsoft/ApplicationInsights-dotnet #2143)」および「Web 分析と診断 (dotnet/aspnetcore #5461)」 (コミュニティ実装へのリンクを含む) 参照してください。 それまでの間、クライアント側の Blazor WebAssembly アプリでは、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}"
@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;
            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

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

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

レンダリング ロジック

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

レンダリング ロジックは例外をスローすることがあります。 このシナリオの例は、@someObject.PropertyName が評価されても @someObjectnull であるときに発生しています。

レンダリング ロジックで 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 ステートメントを使用して、例外をトラップします。

ユーザー コードで例外のトラップと処理が行われない場合は、フレームワークによって例外がログに記録されます。

コンポーネントの廃棄

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

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

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

JavaScript 相互運用

IJSRuntime.InvokeAsync を使用すると、.NET コードによって、ユーザーのブラウザーで JavaScript ランタイムの非同期呼び出しを行えます。

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

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

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

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

これらのシナリオでは、通常、スレッドによって以下のことが試みられます。

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

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

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

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

警告

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

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

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

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

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

その他のリソース

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

開発中の詳細なエラー

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

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

このエラー処理エクスペリエンスの UI は、Blazor プロジェクト テンプレートの一部です。

Blazor Server アプリでは、Pages/_Host.cshtml ファイルでエクスペリエンスをカスタマイズします。

<div id="blazor-error-ui">
    <environment include="Staging,Production">
        An error has occurred. This application may no longer respond until reloaded.
    </environment>
    <environment include="Development">
        An unhandled exception has occurred. See browser dev tools for details.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

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

Blazor Server の詳細な回線エラー

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

CircuitOptions.DetailedErrorstrue に設定します。 使用例を含む詳細については、「ASP.NET Core Blazor SignalR ガイダンス」を参照してください。

CircuitOptions.DetailedErrors を設定する代わりに、アプリの開発環境設定ファイル (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"
    }
  }
}

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

警告

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

ハンドルされない例外に対して Blazor Server アプリがどのように反応するか

Blazor Server はステートフルなフレームワークです。 ユーザーはアプリを操作するときに、回線 と呼ばれる、サーバーへの接続を維持します。 回線では、アクティブなコンポーネント インスタンスに加えて、次のような状態の他の多くの側面が保持されます。

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

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

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

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

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

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

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

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

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

Blazor is a single-page application (SPA) client-side framework. The browser serves as the app's host and thus acts as the processing pipeline for individual Razor components based on URI requests for navigation and static assets. Unlike ASP.NET Core apps that run on the server with a middleware processing pipeline, there is no middleware pipeline that processes requests for Razor components that can be leveraged for global error handling. However, an app can use an error processing component as a cascading value to process errors in a centralized way.

The following Error component passes itself as a CascadingValue to child components. The following example merely logs the error, but methods of the component can process errors in any way required by the app, including through the use of multiple error processing methods. An advantage of using a component over using an injected service or a custom logger implementation is that a cascaded component can render content and apply CSS styles when an error occurs.

Shared/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);
    }
}

In the App component, wrap the Router component with the Error component. This permits the Error component to cascade down to any component of the app where the Error component is received as a CascadingParameter.

App.razor:

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

To process errors in a component:

  • Designate the Error component as a CascadingParameter in the @code block:

    [CascadingParameter]
    public Error Error { get; set; }
    
  • Call an error processing method in any catch block with an appropriate exception type. The example Error component only offers a single ProcessError method, but the error processing component can provide any number of error processing methods to address alternative error processing requirements throughout the app.

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

Using the preceding example Error component and ProcessError method, the browser's developer tools console indicates the trapped, logged error:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: Object reference not set to an instance of an object.

If the ProcessError method directly participates in rendering, such as showing a custom error message bar or changing the CSS styles of the rendered elements, call StateHasChanged at the end of the ProcessErrors method to rerender the UI.

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

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

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

通常、開発中は、Blazor Server アプリによって、デバッグの助けになるよう、例外の完全な詳細情報がブラウザーのコンソールに送信されます。 運用時には、詳細なエラーはクライアントに送信されませんが、例外の詳細がサーバーに記録されます。

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

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

† Blazor アプリ用の Web API バックエンド アプリであるサーバー側 ASP.NET Core アプリに適用されます。

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

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

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

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

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

実行されたいずれかのコンストラクターまたは任意の [Inject] プロパティのセッターがハンドルされない例外をスローすると、Blazor Server の回線が失敗します。 フレームワークではコンポーネントをインスタンス化できないため、この例外は致命的です。 コンストラクターのロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップする必要があります。

ライフサイクル メソッド

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

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

  • ProductRepository.GetProductByIdAsync メソッドでスローされた例外は try-catch ステートメントによって処理されます。
  • catch ブロックの実行時には:
    • loadFailedtrue に設定されます。これがユーザーにエラー メッセージを表示するために使われます。
    • エラーがログに記録されます。
@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;
            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

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

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

レンダリング ロジック

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

レンダリング ロジックは例外をスローすることがあります。 このシナリオの例は、@someObject.PropertyName が評価されても @someObjectnull であるときに発生しています。 レンダリング ロジックによってスローされるハンドルされない例外は、Blazor Server 回線にとって致命的です。

レンダリング ロジックで 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

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

イベント ハンドラーがハンドルされない例外をスローした場合 (たとえば、データベース クエリが失敗した)、例外は Blazor Server 回線にとって致命的です。 アプリが外部の理由で失敗する可能性のあるコードを呼び出す場合は、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップします。

ユーザー コードで例外のトラップと処理が行われない場合は、フレームワークによって例外がログに記録され、回線が終了されます。

コンポーネントの廃棄

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

コンポーネントの Dispose メソッドがハンドルされない例外をスローした場合、例外は Blazor Server 回線にとって致命的です。 破棄のロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップする必要があります。

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

JavaScript 相互運用

IJSRuntime.InvokeAsync を使用すると、.NET コードによって、ユーザーのブラウザーで JavaScript ランタイムの非同期呼び出しを行えます。

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

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

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

  • 例外は Blazor Server 回線にとって致命的なものとして扱われません。
  • JavaScript 側の Promise が拒否されます。

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

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

Blazor Server のプリレンダリング

レンダリングされた HTML マークアップがユーザーの初期 HTTP 要求の一部として返されるように、コンポーネント タグ ヘルパーを使用して Blazor コンポーネントを事前レンダリングすることができます。 これは以下によって機能します。

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

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

  • 例外は回線にとって致命的です。
  • その例外は、ComponentTagHelper タグ ヘルパーの呼び出し履歴から破棄されます。 そのため、開発者コードによって例外が明示的にキャッチされない限り、HTTP 要求全体が失敗します。

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

これらのシナリオでは、影響を受けた Blazor Server 回線が失敗し、スレッドでは通常、以下のことが試みられます。

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

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

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

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

警告

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

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

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

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

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

その他のリソース

† Blazor アプリ用の Web API バックエンド アプリであるサーバー側 ASP.NET Core アプリに適用されます。

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

開発中の詳細なエラー

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

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

このエラー処理エクスペリエンスの UI は、Blazor プロジェクト テンプレートの一部です。

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

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

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

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

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

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

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

Blazor is a single-page application (SPA) client-side framework. The browser serves as the app's host and thus acts as the processing pipeline for individual Razor components based on URI requests for navigation and static assets. Unlike ASP.NET Core apps that run on the server with a middleware processing pipeline, there is no middleware pipeline that processes requests for Razor components that can be leveraged for global error handling. However, an app can use an error processing component as a cascading value to process errors in a centralized way.

The following Error component passes itself as a CascadingValue to child components. The following example merely logs the error, but methods of the component can process errors in any way required by the app, including through the use of multiple error processing methods. An advantage of using a component over using an injected service or a custom logger implementation is that a cascaded component can render content and apply CSS styles when an error occurs.

Shared/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);
    }
}

In the App component, wrap the Router component with the Error component. This permits the Error component to cascade down to any component of the app where the Error component is received as a CascadingParameter.

App.razor:

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

To process errors in a component:

  • Designate the Error component as a CascadingParameter in the @code block:

    [CascadingParameter]
    public Error Error { get; set; }
    
  • Call an error processing method in any catch block with an appropriate exception type. The example Error component only offers a single ProcessError method, but the error processing component can provide any number of error processing methods to address alternative error processing requirements throughout the app.

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

Using the preceding example Error component and ProcessError method, the browser's developer tools console indicates the trapped, logged error:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: Object reference not set to an instance of an object.

If the ProcessError method directly participates in rendering, such as showing a custom error message bar or changing the CSS styles of the rendered elements, call StateHasChanged at the end of the ProcessErrors method to rerender the UI.

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

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

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

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

† Blazor WebAssembly アプリをサポートする Application Insights のネイティブ機能と、Google Analytics に対する Blazor フレームワークのネイティブ サポートは、これらのテクノロジの今後のリリースで利用できるようになる可能性があります。 詳細については、「Blazor WASM クライアント側での App Insights のサポート (microsoft/ApplicationInsights-dotnet #2143)」および「Web 分析と診断 (dotnet/aspnetcore #5461)」 (コミュニティ実装へのリンクを含む) 参照してください。 それまでの間、クライアント側の Blazor WebAssembly アプリでは、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}"
@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;
            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

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

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

レンダリング ロジック

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

レンダリング ロジックは例外をスローすることがあります。 このシナリオの例は、@someObject.PropertyName が評価されても @someObjectnull であるときに発生しています。

レンダリング ロジックで 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 Person();

    ...
}

イベント ハンドラー

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

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

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

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

ユーザー コードで例外のトラップと処理が行われない場合は、フレームワークによって例外がログに記録されます。

コンポーネントの廃棄

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

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

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

JavaScript 相互運用

IJSRuntime.InvokeAsync を使用すると、.NET コードによって、ユーザーのブラウザーで JavaScript ランタイムの非同期呼び出しを行えます。

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

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

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

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

これらのシナリオでは、通常、スレッドによって以下のことが試みられます。

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

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

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

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

警告

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

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

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

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

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

その他のリソース

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

開発中の詳細なエラー

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

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

このエラー処理エクスペリエンスの UI は、Blazor プロジェクト テンプレートの一部です。

Blazor Server アプリでは、Pages/_Host.cshtml ファイルでエクスペリエンスをカスタマイズします。

<div id="blazor-error-ui">
    <environment include="Staging,Production">
        An error has occurred. This application may no longer respond until reloaded.
    </environment>
    <environment include="Development">
        An unhandled exception has occurred. See browser dev tools for details.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

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

Blazor Server の詳細な回線エラー

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

CircuitOptions.DetailedErrorstrue に設定します。 使用例を含む詳細については、「ASP.NET Core Blazor SignalR ガイダンス」を参照してください。

CircuitOptions.DetailedErrors を設定する代わりに、アプリの開発環境設定ファイル (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"
    }
  }
}

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

警告

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

ハンドルされない例外に対して Blazor Server アプリがどのように反応するか

Blazor Server はステートフルなフレームワークです。 ユーザーはアプリを操作するときに、回線 と呼ばれる、サーバーへの接続を維持します。 回線では、アクティブなコンポーネント インスタンスに加えて、次のような状態の他の多くの側面が保持されます。

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

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

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

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

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

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

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

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

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

Blazor is a single-page application (SPA) client-side framework. The browser serves as the app's host and thus acts as the processing pipeline for individual Razor components based on URI requests for navigation and static assets. Unlike ASP.NET Core apps that run on the server with a middleware processing pipeline, there is no middleware pipeline that processes requests for Razor components that can be leveraged for global error handling. However, an app can use an error processing component as a cascading value to process errors in a centralized way.

The following Error component passes itself as a CascadingValue to child components. The following example merely logs the error, but methods of the component can process errors in any way required by the app, including through the use of multiple error processing methods. An advantage of using a component over using an injected service or a custom logger implementation is that a cascaded component can render content and apply CSS styles when an error occurs.

Shared/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);
    }
}

In the App component, wrap the Router component with the Error component. This permits the Error component to cascade down to any component of the app where the Error component is received as a CascadingParameter.

App.razor:

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

To process errors in a component:

  • Designate the Error component as a CascadingParameter in the @code block:

    [CascadingParameter]
    public Error Error { get; set; }
    
  • Call an error processing method in any catch block with an appropriate exception type. The example Error component only offers a single ProcessError method, but the error processing component can provide any number of error processing methods to address alternative error processing requirements throughout the app.

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

Using the preceding example Error component and ProcessError method, the browser's developer tools console indicates the trapped, logged error:

fail: BlazorSample.Shared.Error[0] Error:ProcessError - Type: System.NullReferenceException Message: Object reference not set to an instance of an object.

If the ProcessError method directly participates in rendering, such as showing a custom error message bar or changing the CSS styles of the rendered elements, call StateHasChanged at the end of the ProcessErrors method to rerender the UI.

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

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

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

通常、開発中は、Blazor Server アプリによって、デバッグの助けになるよう、例外の完全な詳細情報がブラウザーのコンソールに送信されます。 運用時には、詳細なエラーはクライアントに送信されませんが、例外の詳細がサーバーに記録されます。

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

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

† Blazor アプリ用の Web API バックエンド アプリであるサーバー側 ASP.NET Core アプリに適用されます。

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

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

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

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

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

実行されたいずれかのコンストラクターまたは任意の [Inject] プロパティのセッターがハンドルされない例外をスローすると、Blazor Server の回線が失敗します。 フレームワークではコンポーネントをインスタンス化できないため、この例外は致命的です。 コンストラクターのロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップする必要があります。

ライフサイクル メソッド

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

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

  • ProductRepository.GetProductByIdAsync メソッドでスローされた例外は try-catch ステートメントによって処理されます。
  • catch ブロックの実行時には:
    • loadFailedtrue に設定されます。これがユーザーにエラー メッセージを表示するために使われます。
    • エラーがログに記録されます。
@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;
            details = await ProductRepository.GetProductByIdAsync(ProductId);
        }
        catch (Exception ex)
        {
            loadFailed = true;
            Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
        }
    }

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

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

レンダリング ロジック

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

レンダリング ロジックは例外をスローすることがあります。 このシナリオの例は、@someObject.PropertyName が評価されても @someObjectnull であるときに発生しています。 レンダリング ロジックによってスローされるハンドルされない例外は、Blazor Server 回線にとって致命的です。

レンダリング ロジックで 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 Person();

    ...
}

イベント ハンドラー

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

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

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

イベント ハンドラーがハンドルされない例外をスローした場合 (たとえば、データベース クエリが失敗した)、例外は Blazor Server 回線にとって致命的です。 アプリが外部の理由で失敗する可能性のあるコードを呼び出す場合は、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップします。

ユーザー コードで例外のトラップと処理が行われない場合は、フレームワークによって例外がログに記録され、回線が終了されます。

コンポーネントの廃棄

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

コンポーネントの Dispose メソッドがハンドルされない例外をスローした場合、例外は Blazor Server 回線にとって致命的です。 破棄のロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch ステートメントを使用して、例外をトラップする必要があります。

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

JavaScript 相互運用

IJSRuntime.InvokeAsync を使用すると、.NET コードによって、ユーザーのブラウザーで JavaScript ランタイムの非同期呼び出しを行えます。

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

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

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

  • 例外は Blazor Server 回線にとって致命的なものとして扱われません。
  • JavaScript 側の Promise が拒否されます。

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

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

Blazor Server のプリレンダリング

レンダリングされた HTML マークアップがユーザーの初期 HTTP 要求の一部として返されるように、コンポーネント タグ ヘルパーを使用して Blazor コンポーネントを事前レンダリングすることができます。 これは以下によって機能します。

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

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

  • 例外は回線にとって致命的です。
  • その例外は、ComponentTagHelper タグ ヘルパーの呼び出し履歴から破棄されます。 そのため、開発者コードによって例外が明示的にキャッチされない限り、HTTP 要求全体が失敗します。

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

これらのシナリオでは、影響を受けた Blazor Server 回線が失敗し、スレッドでは通常、以下のことが試みられます。

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

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

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

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

警告

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

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

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

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

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

その他のリソース

† Blazor アプリ用の Web API バックエンド アプリであるサーバー側 ASP.NET Core アプリに適用されます。