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

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

開発中の詳細なエラー

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

詳細な回線エラー

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

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

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

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 に設定することもできます。

警告

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

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

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

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

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

Blazor Server のハンドルされない例外

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

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

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

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

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

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

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

エラー境界

Blazor は、シングルページ アプリケーション (SPA) のクライアント側フレームワークです。 このブラウザーは、アプリのホストとして機能するため、ナビゲーションと静的アセットの URI 要求に基づいて、個々の Razor コンポーネントの処理パイプラインとして機能します。 ミドルウェア処理パイプラインを使用してサーバー上で実行される ASP.NET Core アプリとは異なり、グローバル エラー処理に利用できる Razor コンポーネントの要求を処理するミドルウェア パイプラインはありません。 ただし、アプリはエラー処理コンポーネントをカスケード値として使用して、一元的な方法でエラーを処理できます。

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

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

エラー境界を定義するには、ErrorBoundary コンポーネントを使用して、既存のコンテンツをラップします。 たとえば、アプリのメイン レイアウトの本文コンテンツの周囲にエラー境界を追加できます。

Shared/MainLayout.razor:

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

アプリは正常に機能し続けますが、ハンドルされない例外がエラー境界によって処理されます。

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

Pages/Counter.razor:

private void IncrementCount()
{
    currentCount++;

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

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

  • 例外がエラー境界によって処理されます。
  • エラー UI がレンダリングされます (An error has occurred.)。

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

また、ErrorContent プロパティを設定することで、既定のエラー コンテンツを変更できます。

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

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

...

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

...

@code {
    private ErrorBoundary? errorBoundary;

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

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

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

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

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

注意

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

App コンポーネントで 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: BlazorSample.Shared.Error[0]
Error:ProcessError - Type: System.InvalidOperationException Message: Current count is over five!

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

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

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

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

Note

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

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

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

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

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

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

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

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

    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 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

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

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

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

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

コンポーネントの廃棄

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

Blazor Server アプリで、コンポーネントの Dispose メソッドがハンドルされない例外をスローした場合、例外はアプリの回線にとって致命的です。

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

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

JavaScript 相互運用

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

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

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

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

  • Blazor Server アプリでは、例外はアプリの回線にとって致命的なものとして処理 "されません"。
  • JS 側の Promise は拒否されます。

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

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

  • ASP.NET Core で .NET メソッドから JavaScript 関数を呼び出す
  • ASP.NET Core で JavaScript 関数から .NET メソッドを呼び出す

プリレンダリング

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

Blazor Server で、プリレンダリングが次のように機能します。

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

プリレンダリングされた Blazor WebAssembly で、プリレンダリングが次のように機能します。

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

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

  • Blazor Sever アプリで、例外は回線にとって致命的です。 プリレンダリングされた Blazor WebAssembly アプリでは、例外によってコンポーネントがレンダリングされません。
  • その例外は、ComponentTagHelper の呼び出し履歴から破棄されます。

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

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

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

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

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

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

警告

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

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

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

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

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

その他のリソース

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

Blazor WebAssembly アプリの開発中の詳細なエラー

開発中に 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 は、シングルページ アプリケーション (SPA) のクライアント側フレームワークです。 このブラウザーは、アプリのホストとして機能するため、ナビゲーションと静的アセットの URI 要求に基づいて、個々の Razor コンポーネントの処理パイプラインとして機能します。 ミドルウェア処理パイプラインを使用してサーバー上で実行される ASP.NET Core アプリとは異なり、グローバル エラー処理に利用できる Razor コンポーネントの要求を処理するミドルウェア パイプラインはありません。 ただし、アプリはエラー処理コンポーネントをカスケード値として使用して、一元的な方法でエラーを処理できます。

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

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

注意

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 Server アプリの SignalR 接続が切断されることはありません。 すべてのハンドルされない例外は回線にとって致命的です。 詳細については、ハンドルされない例外への Blazor Server アプリの対応方法に関する前のセクションを参照してください。

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

ハンドルされない例外が発生した場合、例外は、サービス コンテナー内に構成されている 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 WebAssembly アプリでエラーが発生する可能性のある場所

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

コンポーネントのインスタンス化 (Blazor WebAssembly)

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

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

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

ライフサイクル メソッド (Blazor WebAssembly)

コンポーネントの有効期間の間は、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);
        }
    }

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

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

レンダリング ロジック (Blazor WebAssembly)

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();

    ...
}

イベント ハンドラー (Blazor WebAssembly)

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

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

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

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

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

コンポーネントの廃棄 (Blazor WebAssembly)

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

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

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

JavaScript 相互運用 (Blazor WebAssembly)

IJSRuntime は Blazor フレームワークによって登録されます。 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 側か、どちらでエラー処理コードを使用するかを選択できます。

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

  • ASP.NET Core で .NET メソッドから JavaScript 関数を呼び出す
  • ASP.NET Core で JavaScript 関数から .NET メソッドを呼び出す

Blazor Server アプリの開発中の詳細なエラー

開発中に 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 BlazorSignalR のガイダンス」をご覧ください。

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

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

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

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

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

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

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

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

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

Blazor Server アプリでエラーが発生する可能性のある場所

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

コンポーネントのインスタンス化 (Blazor Server)

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

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

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

ライフサイクル メソッド (Blazor Server)

コンポーネントの有効期間の間は、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);
        }
    }

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

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

レンダリング ロジック (Blazor Server)

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();

    ...
}

イベント ハンドラー (Blazor Server)

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

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

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

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

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

コンポーネントの廃棄 (Blazor Server)

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

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

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

JavaScript 相互運用 (Blazor Server)

IJSRuntime は Blazor フレームワークによって登録されます。 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 側か、どちらでエラー処理コードを使用するかを選択できます。

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

  • ASP.NET Core で .NET メソッドから JavaScript 関数を呼び出す
  • ASP.NET Core で JavaScript 関数から .NET メソッドを呼び出す

プリレンダリング (Blazor Server)

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

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

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

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

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

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

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

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

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

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

警告

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

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

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

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

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

その他のリソース

Blazor WebAssembly

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

Blazor Server

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

Blazor WebAssembly アプリの開発中の詳細なエラー

開発中に 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 は、シングルページ アプリケーション (SPA) のクライアント側フレームワークです。 このブラウザーは、アプリのホストとして機能するため、ナビゲーションと静的アセットの URI 要求に基づいて、個々の Razor コンポーネントの処理パイプラインとして機能します。 ミドルウェア処理パイプラインを使用してサーバー上で実行される ASP.NET Core アプリとは異なり、グローバル エラー処理に利用できる Razor コンポーネントの要求を処理するミドルウェア パイプラインはありません。 ただし、アプリはエラー処理コンポーネントをカスケード値として使用して、一元的な方法でエラーを処理できます。

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

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

注意

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 Server アプリの SignalR 接続が切断されることはありません。 すべてのハンドルされない例外は回線にとって致命的です。 詳細については、ハンドルされない例外への Blazor Server アプリの対応方法に関する前のセクションを参照してください。

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

ハンドルされない例外が発生した場合、例外は、サービス コンテナー内に構成されている 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 WebAssembly アプリでエラーが発生する可能性のある場所

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

コンポーネントのインスタンス化 (Blazor WebAssembly)

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

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

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

ライフサイクル メソッド (Blazor WebAssembly)

コンポーネントの有効期間の間は、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);
        }
    }

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

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

レンダリング ロジック (Blazor WebAssembly)

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();

    ...
}

イベント ハンドラー (Blazor WebAssembly)

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

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

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

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

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

コンポーネントの廃棄 (Blazor WebAssembly)

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

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

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

JavaScript 相互運用 (Blazor WebAssembly)

IJSRuntime は Blazor フレームワークによって登録されます。 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 側か、どちらでエラー処理コードを使用するかを選択できます。

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

  • ASP.NET Core で .NET メソッドから JavaScript 関数を呼び出す
  • ASP.NET Core で JavaScript 関数から .NET メソッドを呼び出す

Blazor Server アプリの開発中の詳細なエラー

開発中に 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 BlazorSignalR のガイダンス」をご覧ください。

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

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

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

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

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

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

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

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

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

Blazor Server アプリでエラーが発生する可能性のある場所

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

コンポーネントのインスタンス化 (Blazor Server)

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

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

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

ライフサイクル メソッド (Blazor Server)

コンポーネントの有効期間の間は、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);
        }
    }

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

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

レンダリング ロジック (Blazor Server)

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();

    ...
}

イベント ハンドラー (Blazor Server)

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

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

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

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

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

コンポーネントの廃棄 (Blazor Server)

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

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

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

JavaScript 相互運用 (Blazor Server)

IJSRuntime は Blazor フレームワークによって登録されます。 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 側か、どちらでエラー処理コードを使用するかを選択できます。

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

  • ASP.NET Core で .NET メソッドから JavaScript 関数を呼び出す
  • ASP.NET Core で JavaScript 関数から .NET メソッドを呼び出す

プリレンダリング (Blazor Server)

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

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

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

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

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

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

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

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

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

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

警告

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

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

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

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

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

その他のリソース

Blazor WebAssembly

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

Blazor Server

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

開発中の詳細なエラー

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

詳細な回線エラー

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

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

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

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 に設定することもできます。

警告

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

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

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

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

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

Blazor Server のハンドルされない例外

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

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

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

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

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

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

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

エラー境界

Blazor は、シングルページ アプリケーション (SPA) のクライアント側フレームワークです。 このブラウザーは、アプリのホストとして機能するため、ナビゲーションと静的アセットの URI 要求に基づいて、個々の Razor コンポーネントの処理パイプラインとして機能します。 ミドルウェア処理パイプラインを使用してサーバー上で実行される ASP.NET Core アプリとは異なり、グローバル エラー処理に利用できる Razor コンポーネントの要求を処理するミドルウェア パイプラインはありません。 ただし、アプリはエラー処理コンポーネントをカスケード値として使用して、一元的な方法でエラーを処理できます。

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

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

エラー境界を定義するには、ErrorBoundary コンポーネントを使用して、既存のコンテンツをラップします。 たとえば、アプリのメイン レイアウトの本文コンテンツの周囲にエラー境界を追加できます。

Shared/MainLayout.razor:

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

アプリは正常に機能し続けますが、ハンドルされない例外がエラー境界によって処理されます。

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

Pages/Counter.razor:

private void IncrementCount()
{
    currentCount++;

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

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

  • 例外がエラー境界によって処理されます。
  • エラー UI がレンダリングされます (An error has occurred.)。

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

また、ErrorContent プロパティを設定することで、既定のエラー コンテンツを変更できます。

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

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

...

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

...

@code {
    private ErrorBoundary? errorBoundary;

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

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

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

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

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

注意

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

App コンポーネントで 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: BlazorSample.Shared.Error[0]
Error:ProcessError - Type: System.InvalidOperationException Message: Current count is over five!

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

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

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

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

Note

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

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

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

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

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

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

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

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

    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 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

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

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

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

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

コンポーネントの廃棄

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

Blazor Server アプリで、コンポーネントの Dispose メソッドがハンドルされない例外をスローした場合、例外はアプリの回線にとって致命的です。

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

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

JavaScript 相互運用

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

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

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

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

  • Blazor Server アプリでは、例外はアプリの回線にとって致命的なものとして処理 "されません"。
  • JS 側の Promise は拒否されます。

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

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

  • ASP.NET Core で .NET メソッドから JavaScript 関数を呼び出す
  • ASP.NET Core で JavaScript 関数から .NET メソッドを呼び出す

プリレンダリング

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

Blazor Server で、プリレンダリングが次のように機能します。

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

プリレンダリングされた Blazor WebAssembly で、プリレンダリングが次のように機能します。

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

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

  • Blazor Sever アプリで、例外は回線にとって致命的です。 プリレンダリングされた Blazor WebAssembly アプリでは、例外によってコンポーネントがレンダリングされません。
  • その例外は、ComponentTagHelper の呼び出し履歴から破棄されます。

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

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

高度なシナリオ

再帰的レンダリング

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

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

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

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

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

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

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

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

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

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

警告

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

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

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

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

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

その他のリソース

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