ASP.NET Core Blazor アプリのエラーを処理する
注意
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
この記事では、ハンドルされない例外を Blazor で管理する方法と、エラーを検出して処理するアプリを開発する方法について説明します。
開発中の詳細なエラー
開発中に Blazor アプリが正常に機能していない場合、アプリからの詳細なエラー情報を受け取ることで、問題のトラブルシューティングと修正に役立ちます。 エラーが発生すると、Blazor アプリによって画面の下部に薄い黄色のバーが表示されます。
- 開発中は、バーによってブラウザー コンソールが表示され、そこで例外を確認できます。
- 運用環境では、バーによって、エラーが発生したことがユーザーに通知され、ブラウザーの更新が推奨されます。
このエラー処理エクスペリエンスの UI は、Blazor プロジェクト テンプレートの一部です。 Blazor プロジェクト テンプレートのすべてのバージョンが、エラー UI の内容をキャッシュしないようにブラウザーに通知するために data-nosnippet
属性を使っているわけではありません。ただし、Blazor ドキュメントのすべてのバージョンがこの属性を適用しています。
Blazor Web App では、MainLayout
コンポーネントのエクスペリエンスをカスタマイズします。 環境タグ ヘルパー (<environment include="Production">...</environment>
など) は Razor コンポーネントではサポートされていないため、次の例では IHostEnvironment を挿入して、さまざまな環境のエラー メッセージを構成します。
MainLayout.razor
の上部:
@inject IHostEnvironment HostEnvironment
Blazor エラー UI マークアップを作成または変更します。
<div id="blazor-error-ui" data-nosnippet>
@if (HostEnvironment.IsProduction())
{
<span>An error has occurred.</span>
}
else
{
<span>An unhandled exception occurred.</span>
}
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
Blazor Server アプリでは、Pages/_Host.cshtml
ファイルでエクスペリエンスをカスタマイズします。 次の例では、環境タグ ヘルパーを使用して、さまざまな環境のエラー メッセージを構成します。
Blazor Server アプリでは、Pages/_Layout.cshtml
ファイルでエクスペリエンスをカスタマイズします。 次の例では、環境タグ ヘルパーを使用して、さまざまな環境のエラー メッセージを構成します。
Blazor Server アプリでは、Pages/_Host.cshtml
ファイルでエクスペリエンスをカスタマイズします。 次の例では、環境タグ ヘルパーを使用して、さまざまな環境のエラー メッセージを構成します。
Blazor エラー UI マークアップを作成または変更します。
<div id="blazor-error-ui" data-nosnippet>
<environment include="Staging,Production">
An error has occurred.
</environment>
<environment include="Development">
An unhandled exception occurred.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
Blazor WebAssembly アプリでは、wwwroot/index.html
ファイルでエクスペリエンスをカスタマイズします。
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
blazor-error-ui
要素は、通常、アプリの自動生成スタイルシート に blazor-error-ui
CSS クラスの display: none
スタイルが存在するため、非表示になります。 エラーが発生すると、フレームワークによって要素に display: block
が適用されます。
blazor-error-ui
要素は、blazor-error-ui
フォルダー内のサイトのスタイルシートに blazor-error-ui
CSS クラスの display: none
スタイルが存在するため、通常は非表示になります。 エラーが発生すると、フレームワークによって要素に display: block
が適用されます。
詳細な回線エラー
このセクションは、回線上で動作する Blazor Web App に適用されます。
このセクションは Blazor Server アプリに適用されます。
クライアント側のエラーには、呼び出し履歴は含まれず、エラーの原因についての詳細は提供されませんが、サーバー ログにはこのような情報が含まれています。 開発目的で、詳細なエラーを有効にすることによって、機密性の高い回線エラー情報をクライアントが利用できるようにすることができます。
CircuitOptions.DetailedErrors を true
に設定します。 詳細と例については、「ASP.NET Core BlazorSignalR のガイダンス」をご覧ください。
CircuitOptions.DetailedErrors を設定する代わりに、アプリの Development
環境設定ファイル (appsettings.Development.json
) で DetailedErrors
構成キーを true
に設定することもできます。 さらに、SignalR の詳細なログを記録するには、SignalR のサーバー側ログ記録 (Microsoft.AspNetCore.SignalR
) を Debug または Trace に設定します。
appsettings.Development.json
:
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.SignalR": "Debug"
}
}
}
Development
/Staging
環境のサーバーまたはローカル システムで、ASPNETCORE_DETAILEDERRORS
環境変数の値を true
にすることで、DetailedErrors 構成キーを true
に設定することもできます。
警告
インターネット上のクライアントにはエラー情報を常に公開しないようにします。これは、セキュリティ上のリスクです。
Razor コンポーネントのサーバー側レンダリングの詳細なエラー
このセクションは Blazor Web App に適用されます。
RazorComponentsServiceOptions.DetailedErrors オプションを使用して、Razor コンポーネントのサーバー側レンダリングのエラーに関する詳細情報の生成を制御します。 既定値は false
です。
次の例では、詳細なエラーが有効になっています。
builder.Services.AddRazorComponents(options =>
options.DetailedErrors = builder.Environment.IsDevelopment());
警告
詳細なエラーは Development
環境でのみ有効にしてください。 詳細なエラーには、悪意のあるユーザーが攻撃に使用できる、アプリに関する機密情報が含まれている可能性があります。
前の例では、IsDevelopment によって返されるの値に基づいて DetailedErrors の値を設定することで、安全性の程度を提供します。 アプリが Development
環境内にある場合は、DetailedErrors が true
に設定されます。 Development
環境内のパブリック サーバー環境で運用アプリをホストできるため、この方法は確実ではありません。
ハンドルされない例外を開発者コードで管理する
エラー後にアプリを続行するには、アプリがエラー処理ロジックを備えている必要があります。 この記事の後のセクションでは、ハンドルされない例外の潜在的原因について説明します。
運用環境では、フレームワークの例外メッセージやスタック トレースを UI に表示しないでください。 例外メッセージやスタック トレースを表示すると以下の可能性があります。
- エンド ユーザーに機密情報が開示される。
- 悪意のあるユーザーが、アプリ、サーバー、またはネットワークのセキュリティを侵害する可能性のある脆弱性をアプリの中で発見する助けになる。
回線の未処理例外
このセクションは、回線上で動作するサーバー側アプリに適用されます。
サーバー対話機能が有効になっている Razor コンポーネントはサーバー上でステートフルです。 ユーザーはサーバー上のコンポーネントを操作している間、回線と呼ばれるサーバーへの接続を維持します。 回線では、アクティブなコンポーネント インスタンスに加えて、次のような状態の他の多くの側面が保持されます。
- コンポーネントの表示される最新の出力。
- クライアント側のイベントによってトリガーされる可能性がある、イベント処理デリゲートの現在のセット。
ユーザーが複数のブラウザー タブでアプリを開いた場合、ユーザーは複数の独立した回線を作成します。
Blazor は、ハンドルされない例外が発生した回線に対して、ほとんどの例外を致命的として処理します。 ハンドルされない例外のために回線が終了された場合、ユーザーはページを再読み込みして新しい回線を作成するだけで、アプリの操作を続行できます。 他のユーザーや他のブラウザー タブの回線である、終了された回線以外の回線は影響を受けません。 このシナリオは、クラッシュするデスクトップ アプリに似ています。 クラッシュしたアプリを再起動する必要がありますが、他のアプリは影響を受けません。
以下の理由でハンドルされない例外が発生すると、回線はフレームワークによって終了されます。
- ハンドルされない例外によって、回線が未定義の状態のままになることがよくあります。
- ハンドルされない例外の後は、アプリの通常動作を保証できません。
- 回線が未定義状態のままになっていると、アプリにセキュリティの脆弱性が発生するおそれがあります。
グローバル例外処理
例外をグローバルに処理するためのアプローチについては、以下のセクションを参照してください。
- エラー境界: すべての Blazor アプリに適用されます。
- 代替グローバル例外処理: グローバル対話型レンダー モードを採用している Blazor Server、Blazor WebAssembly、Blazor Web App (8.0 以降) に適用されます。
エラー境界
"エラー境界" には、例外を処理するための便利な方法があります。 ErrorBoundary コンポーネント:
- エラーが発生しなかった場合は、子コンテンツをレンダリングします。
- エラー境界内のいずれのコンポーネントによってもハンドルされない例外がスローされた場合は、エラー UI をレンダリングします。
エラー境界を定義するには、ErrorBoundary コンポーネントを使用して、1 つ以上の他のコンポーネントをラップします。 エラー境界は、ラップするコンポーネントによってスローされるハンドルされない例外を管理します。
<ErrorBoundary>
...
</ErrorBoundary>
エラー境界をグローバルな方法で実装するには、アプリのメイン レイアウトの本文コンテンツの周囲に境界を追加できます。
MainLayout.razor
:
<article class="content px-4">
<ErrorBoundary>
@Body
</ErrorBoundary>
</article>
エラー境界が静的 MainLayout
コンポーネントにのみ適用される Blazor Web App では、境界は静的なサーバー側レンダリング (静的 SSR) 中にのみアクティブになります。 境界は、コンポーネント階層の下位にあるコンポーネントが対話型であるという理由だけでアクティブになるわけではありません。
MainLayout
コンポーネントの Body
パラメーターは、任意のコードでありシリアル化できない RenderFragment デリゲートであるため、対話型レンダリング モードをこのコンポーネントに適用することはできません。 MainLayout
コンポーネントと、コンポーネント階層の下位にある rest のコンポーネントに対して広範にインタラクティビティを有効にするには、アプリは対話型レンダー モードをアプリのルート コンポーネント (通常は App
コンポーネント) の HeadOutlet
および Routes
コンポーネント インスタンスに適用し、グローバル対話型レンダー モードを採用する必要があります。 次の例では、対話型サーバー (InteractiveServer
) レンダリング モードをグローバルに採用しています。
Components/App.razor
:
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />
グローバル インタラクティビティを有効にする必要がない場合は、エラー境界をコンポーネント階層のさらに下に配置します。 留意すべき重要な概念は、エラー境界が配置される場所を問わないということです。
- エラー境界が配置されているコンポーネントが対話型ではない場合、エラー境界は静的 SSR 中にサーバー上でのみアクティブ化できます。 たとえば、コンポーネント ライフサイクル メソッドではエラーがスローされるが、コンポーネント内のユーザー対話によってトリガーされるイベントに対してはスローされない場合は (ボタン クリック ハンドラーによってスローされるエラーなど)、境界をアクティブ化できます。
- エラー境界が配置されているコンポーネントが対話型の場合、エラー境界はラップする対話型コンポーネントをアクティブ化できます。
Note
Blazor WebAssembly アプリのクライアント側レンダリング (CSR) は完全に対話型であるため、上記の考慮事項はスタンドアロン Blazor WebAssembly アプリには関係ありません。
次の例では、埋め込みカウンター コンポーネントによってスローされた例外が、対話型レンダリング モードを採用する Home
コンポーネントのエラー境界によって捕捉されます。
EmbeddedCounter.razor
:
<h1>Embedded Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
if (currentCount > 5)
{
throw new InvalidOperationException("Current count is too big!");
}
}
}
Home.razor
:
@page "/"
@rendermode InteractiveServer
<PageTitle>Home</PageTitle>
<h1>Home</h1>
<ErrorBoundary>
<EmbeddedCounter />
</ErrorBoundary>
次の例では、埋め込みカウンター コンポーネントによってスローされた例外が、Home
コンポーネントのエラー境界によって捕捉されます。
EmbeddedCounter.razor
:
<h1>Embedded Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
if (currentCount > 5)
{
throw new InvalidOperationException("Current count is too big!");
}
}
}
Home.razor
:
@page "/"
<PageTitle>Home</PageTitle>
<h1>Home</h1>
<ErrorBoundary>
<EmbeddedCounter />
</ErrorBoundary>
5 を超える currentCount
に対して、ハンドルされない例外がスローされる場合は、次のようになります。
- エラーは通常どおりログに記録されます (
System.InvalidOperationException: Current count is too big!
)。 - 例外がエラー境界によって処理されます。
- 既定のエラー UI はエラー境界によってレンダリングされます。
ErrorBoundary コンポーネントによって、エラー コンテンツの blazor-error-boundary
CSS クラスを使用して空の <div>
要素がレンダリングされます。 既定の UI の色、テキスト、アイコンは、wwwroot
フォルダー内にあるアプリのスタイルシートで定義されるため、エラー UI を自由にカスタマイズすることができます。
既定のエラー コンテンツを変更するには:
- エラー境界のコンポーネントを ChildContent プロパティでラップします。
- ErrorContent プロパティをエラー コンテンツに設定します。
次の例では、EmbeddedCounter
コンポーネントをラップし、カスタム エラーの内容を提供します。
<ErrorBoundary>
<ChildContent>
<EmbeddedCounter />
</ChildContent>
<ErrorContent>
<p class="errorUI">😈 A rotten gremlin got us. Sorry!</p>
</ErrorContent>
</ErrorBoundary>
前の例では、アプリのスタイルシートには、コンテンツのスタイルを設定するための errorUI
CSS クラスが含まれていると推定されます。 エラーの内容は、ブロックレベルの要素を持たない ErrorContent プロパティからレンダリングされます。 ディビジョン (<div>
) や段落 (<p>
) 要素などのブロックレベルの要素は、エラー内容マークアップをラップできますが、必須ではありません。
必要に応じて、ErrorContent のコンテキスト (@context
) を使用してエラー データを取得します。
<ErrorContent>
@context.HelpLink
</ErrorContent>
ErrorContent はコンテキストに名前を付けることもできます。 次の例では、コンテキストに exception
という名前が付けられています。
<ErrorContent Context="exception">
@exception.HelpLink
</ErrorContent>
警告
インターネット上のクライアントにはエラー情報を常に公開しないようにします。これは、セキュリティ上のリスクです。
エラー境界がアプリのレイアウトで定義されている場合、エラー発生後にユーザーがどのページに移動したかにかかわらず、エラー UI が表示されます。 ほとんどのシナリオで、エラー境界の範囲を狭くすることをお勧めします。 エラー境界の範囲を広く設定する場合は、エラー境界の Recover メソッドを呼び出すことによって、後続のページ ナビゲーション イベントでエラー以外の状態にリセットできます。
MainLayout.razor
の場合:
@ref
属性ディレクティブを使用して、ErrorBoundary から への参照 をキャプチャするためのフィールドを追加します。OnParameterSet
ライフサイクルメソッドでは、ユーザーが別のコンポーネントに移動したときにエラーをクリアするために、Recover を使用してエラー境界で復旧をトリガーできます。
...
<ErrorBoundary @ref="errorBoundary">
@Body
</ErrorBoundary>
...
@code {
private ErrorBoundary? errorBoundary;
protected override void OnParametersSet()
{
errorBoundary?.Recover();
}
}
回復するとエラーが再度スローされるコンポーネントが再レンダリングされるだけの無限ループを避けるために、レンダリング ロジックから Recover を呼び出さないでください。 次の場合にのみ Recover を呼び出します。
- ユーザーは、ボタンの選択などの UI ジェスチャを実行して、手順を再試行するか、新しいコンポーネントに移動することを示します。
- 実行する追加のロジックによっても例外がクリアされます。 コンポーネントが再レンダリングされると、エラーは再発しません。
次の例では、ユーザーがボタンを使用して例外から回復することを許可します。
<ErrorBoundary @ref="errorBoundary">
<ChildContent>
<EmbeddedCounter />
</ChildContent>
<ErrorContent>
<div class="alert alert-danger" role="alert">
<p class="fs-3 fw-bold">😈 A rotten gremlin got us. Sorry!</p>
<p>@context.HelpLink</p>
<button class="btn btn-info" @onclick="_ => errorBoundary?.Recover()">
Clear
</button>
</div>
</ErrorContent>
</ErrorBoundary>
@code {
private ErrorBoundary? errorBoundary;
}
OnErrorAsync をオーバーライドすることで、カスタム処理の ErrorBoundary をサブクラス化することもできます。 次の例では単にエラーをログに記録しますが、任意のエラー処理コードを実装できます。 コードが非同期タスクを待機している場合は、CompletedTask を返す行を削除できます。
CustomErrorBoundary.razor
:
@inherits ErrorBoundary
@inject ILogger<CustomErrorBoundary> Logger
@if (CurrentException is null)
{
@ChildContent
}
else if (ErrorContent is not null)
{
@ErrorContent(CurrentException)
}
@code {
protected override Task OnErrorAsync(Exception ex)
{
Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
return Task.CompletedTask;
}
}
上記の例は、クラスとして実装することもできます。
CustomErrorBoundary.cs
:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace BlazorSample;
public class CustomErrorBoundary : ErrorBoundary
{
[Inject]
ILogger<CustomErrorBoundary> Logger { get; set; } = default!;
protected override Task OnErrorAsync(Exception ex)
{
Logger.LogError(ex, "😈 A rotten gremlin got us. Sorry!");
return Task.CompletedTask;
}
}
コンポーネントで使用される上記の実装のいずれかを次に示します。
<CustomErrorBoundary>
...
</CustomErrorBoundary>
代替のグローバル例外処理
このセクションで説明する方法は、グローバル対話型レンダー モード (InteractiveServer
、InteractiveWebAssembly
、または InteractiveAuto
) を採用する Blazor Server、Blazor WebAssembly、Blazor Web App に適用されます。 この方法は、ページ/コンポーネント単位のレンダリング モードまたは静的サーバー側レンダリング (静的 SSR) を採用する Blazor Web App では機能しません。その理由は、この方法がレンダー モードの境界を超えて機能しない、または静的 SSR を採用するコンポーネントと共には機能しない CascadingValue
/CascadingParameter
に依存しているためです。
エラー境界 (ErrorBoundary) を使用する代わりに、カスタム エラー コンポーネントを CascadingValue
として子コンポーネントに渡すこともできます。 挿入されたサービスまたはカスタム ロガーの実装を使用するよりもコンポーネントを使用する利点は、カスケードされたコンポーネントがコンテンツをレンダリングし、エラーが発生したときに CSS スタイルを適用できることです。
次の ProcessError
コンポーネントの例では、単にエラーをログするだけですが、コンポーネントのメソッドは、アプリが必要とする任意の方法で (複数のエラー処理メソッドを使用するなど) エラーを処理できます。
ProcessError.razor
:
@inject ILogger<ProcessError> Logger
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
public void LogError(Exception ex)
{
Logger.LogError("ProcessError.LogError: {Type} Message: {Message}",
ex.GetType(), ex.Message);
// Call StateHasChanged if LogError directly participates in
// rendering. If LogError only logs or records the error,
// there's no need to call StateHasChanged.
//StateHasChanged();
}
}
注意
RenderFragment の詳細については、ASP.NET Core Razor コンポーネントに関する記事を参照してください。
Blazor Web App でこの方法を使用する場合は、Routes
コンポーネントを開き、Router コンポーネント (<Router>...</Router>
) を ProcessError
コンポーネントでラップします。 これにより、ProcessError
コンポーネントは、ProcessError
コンポーネントが CascadingParameter
として受信されるアプリの任意のコンポーネントにカスケードできるようになります。
Routes.razor
:
<ProcessError>
<Router ...>
...
</Router>
</ProcessError>
Blazor Server または Blazor WebAssembly アプリでこのアプローチを使用する場合は、App
コンポーネントを開き、Router コンポーネント (<Router>...</Router>
) を ProcessError
コンポーネントでラップします。 これにより、ProcessError
コンポーネントは、ProcessError
コンポーネントが CascadingParameter
として受信されるアプリの任意のコンポーネントにカスケードできるようになります。
App.razor
の場合:
<ProcessError>
<Router ...>
...
</Router>
</ProcessError>
コンポーネントのエラーを処理するには:
ProcessError
コンポーネントを@code
ブロック内のCascadingParameter
として指定します。 Blazor プロジェクト テンプレートに基づくアプリのCounter
コンポーネントの例で、次のProcessError
プロパティを追加します。[CascadingParameter] public ProcessError? ProcessError { get; set; }
適切な例外の種類を使用して、任意の
catch
ブロックでエラー処理メソッドを呼び出します。 この例のProcessError
コンポーネントは 1 つのLogError
メソッドのみを提供しますが、エラー処理コンポーネントは、アプリ全体の他のエラー処理要件に対処するために、任意の数のエラー処理メソッドを提供できます。 次のCounter
コンポーネント@code
ブロックの例には、ProcessError
カスケード パラメーターが含まれており、カウントが 5 より大きい場合にログに例外をトラップします。@code { private int currentCount = 0; [CascadingParameter] public ProcessError? ProcessError { get; set; } private void IncrementCount() { try { currentCount++; if (currentCount > 5) { throw new InvalidOperationException("Current count is over five!"); } } catch (Exception ex) { ProcessError?.LogError(ex); } } }
ログされたエラー:
fail: {COMPONENT NAMESPACE}.ProcessError[0]
ProcessError.LogError: System.InvalidOperationException Message: Current count is over five!
カスタム エラー メッセージ バーの表示やレンダリングされた要素の CSS スタイルの変更など、LogError
メソッドがレンダリングに直接関与している場合は、LogError
メソッドの最後で StateHasChanged
を呼び出して、UI を再レンダリングします。
このセクションの方法は、try-catch
ステートメントを使用してエラーを処理するものなので、エラーが発生しても回線が生きていると、クライアントとサーバーの間のアプリの SignalR 接続が切断されることはありません。 その他のハンドルされない例外は、回線にとって致命的なままです。 詳細については、未処理の例外に対する回線の反応に関するセクションを参照してください。
アプリはエラー処理コンポーネントをカスケード値として使用して、一元的な方法でエラーを処理できます。
次の ProcessError
コンポーネントは、それ自体を CascadingValue
として子コンポーネントに渡します。 次の例では、単にエラーをログするだけですが、コンポーネントのメソッドは、アプリが必要とする任意の方法で (複数のエラー処理メソッドを使用するなど) エラーを処理できます。 挿入されたサービスまたはカスタム ロガーの実装を使用するよりもコンポーネントを使用する利点は、カスケードされたコンポーネントがコンテンツをレンダリングし、エラーが発生したときに CSS スタイルを適用できることです。
ProcessError.razor
:
@using Microsoft.Extensions.Logging
@inject ILogger<ProcessError> Logger
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
public void LogError(Exception ex)
{
Logger.LogError("ProcessError.LogError: {Type} Message: {Message}",
ex.GetType(), ex.Message);
}
}
注意
RenderFragment の詳細については、ASP.NET Core Razor コンポーネントに関する記事を参照してください。
App
コンポーネントで Router コンポーネントを ProcessError
コンポーネントでラップします。 これにより、ProcessError
コンポーネントは、ProcessError
コンポーネントが CascadingParameter
として受信されるアプリの任意のコンポーネントにカスケードできるようになります。
App.razor
:
<ProcessError>
<Router ...>
...
</Router>
</ProcessError>
コンポーネントのエラーを処理するには:
ProcessError
コンポーネントを@code
ブロック内のCascadingParameter
として指定します。[CascadingParameter] public ProcessError ProcessError { get; set; }
適切な例外の種類を使用して、任意の
catch
ブロックでエラー処理メソッドを呼び出します。 この例のProcessError
コンポーネントは 1 つのLogError
メソッドのみを提供しますが、エラー処理コンポーネントは、アプリ全体の他のエラー処理要件に対処するために、任意の数のエラー処理メソッドを提供できます。try { ... } catch (Exception ex) { ProcessError.LogError(ex); }
前の例の ProcessError
コンポーネントと LogError
メソッドを使用すると、ブラウザーの開発者ツール コンソールに、トラップされログされた次のエラーが示されます。
fail: {COMPONENT NAMESPACE}.Shared.ProcessError[0]
ProcessError.LogError: System.NullReferenceException Message: Object reference not set to an instance of an object.
カスタム エラー メッセージ バーの表示やレンダリングされた要素の CSS スタイルの変更など、LogError
メソッドがレンダリングに直接関与している場合は、LogError
メソッドの最後で StateHasChanged
を呼び出して、UI を再レンダリングします。
このセクションの方法は、try-catch
ステートメントを使用してエラーを処理するものなので、エラーが発生しても回線が生きていると、クライアントとサーバーの間の Blazor アプリの SignalR 接続が切断されることはありません。 すべてのハンドルされない例外は回線にとって致命的です。 詳細については、未処理の例外に対する回線の反応に関するセクションを参照してください。
永続的プロバイダーを使用してエラーをログに記録する
ハンドルされない例外が発生した場合、例外は、サービス コンテナー内に構成されている ILogger インスタンスにログ記録されます。 Blazor アプリは、コンソール ログ プロバイダーを使用してコンソール出力にログを記録します。 ログ サイズとログ ローテーションを管理するプロバイダーを使用して、サーバー上の場所 (またはクライアント側アプリのバックエンド Web API) にログすることを検討してください。 または、Azure Application Insights (Azure Monitor) などのアプリケーション パフォーマンス管理 (APM) サービスを、アプリで使用することもできます。
メモ
アプリをサポートする Application Insights のネイティブ機能と、Google Analytics に対する Blazor フレームワークのネイティブ サポートは、これらのテクノロジの今後のリリースで利用できるようになる可能性があります。 詳細については、「Blazor WASM クライアント側での App Insights のサポート (microsoft/ApplicationInsights-dotnet #2143)」および「Web 分析と診断 (dotnet/aspnetcore #5461)」 (コミュニティ実装へのリンクを含む) 参照してください。 それまでの間、クライアント側のアプリでは、Application Insights JavaScript SDK とJS相互運用を使用して、クライアント側アプリから Application Insights にエラーを直接記録できます。
回線上で動作する Blazor アプリの開発中、アプリは通常、デバッグを支援するために例外の完全な詳細をブラウザーのコンソールに送信します。 運用時には、詳細なエラーはクライアントに送信されませんが、例外の詳細がサーバーに記録されます。
ログに記録するインシデントと、ログに記録されるインシデントの重大度レベルを決定する必要があります。 悪意のあるユーザーが、意図的にエラーをトリガーできる可能性もあります。 たとえば、製品の詳細を表示するコンポーネントの URL に不明な ProductId
が指定されているエラーのインシデントは、ログに記録しないようにします。 すべてのエラーをログ記録の対象となるインシデントとして扱うことは避けてください。
詳細については、次の記事を参照してください。
‡サーバー側 Blazor アプリと、Blazor 用の Web API バックエンド アプリである他のサーバー側 ASP.NET Core アプリに適用されます。 クライアント側アプリによってクライアント側のエラー情報をトラップして Web API に送信できます。そこで、エラー情報が永続的なログ プロバイダーにログされます。
ハンドルされない例外が発生した場合、例外は、サービス コンテナー内に構成されている ILogger インスタンスにログ記録されます。 Blazor アプリは、コンソール ログ プロバイダーを使用してコンソール出力にログを記録します。 ログ サイズ管理とログ ローテーションを備えたログ プロバイダーを使用するバックエンド Web API にエラー情報を送信することにより、サーバー上のより永続的な場所にログを記録することを検討してください。 または、バックエンド Web API アプリにより、Azure Application Insights (Azure Monitor)† などのアプリケーション パフォーマンス管理 (APM) サービスを使用して、クライアントから受信したエラー情報を記録することもできます。
ログに記録するインシデントと、ログに記録されるインシデントの重大度レベルを決定する必要があります。 悪意のあるユーザーが、意図的にエラーをトリガーできる可能性もあります。 たとえば、製品の詳細を表示するコンポーネントの URL に不明な ProductId
が指定されているエラーのインシデントは、ログに記録しないようにします。 すべてのエラーをログ記録の対象となるインシデントとして扱うことは避けてください。
詳細については、次の記事を参照してください。
†アプリをサポートする Application Insights のネイティブ機能と、Google Analytics に対する Blazor フレームワークのネイティブ サポートは、これらのテクノロジの今後のリリースで利用できるようになる可能性があります。 詳細については、「Blazor WASM クライアント側での App Insights のサポート (microsoft/ApplicationInsights-dotnet #2143)」および「Web 分析と診断 (dotnet/aspnetcore #5461)」 (コミュニティ実装へのリンクを含む) 参照してください。 それまでの間、クライアント側のアプリでは、Application Insights JavaScript SDK とJS相互運用を使用して、クライアント側アプリから Application Insights にエラーを直接記録できます。
‡Blazor アプリ用の Web API バックエンド アプリであるサーバー側 ASP.NET Core アプリに適用されます。 クライアント側アプリによってエラー情報をトラップして Web API に送信します。そこで、エラー情報が永続的なログプロバイダーに記録されます。
エラーが発生する可能性のある場所
次のいずれかの場所で、フレームワークとアプリのコードにより、ハンドルされない例外がトリガーされる場合があります。詳細については、この記事の以降のセクションで説明します。
コンポーネントのインスタンス化
Blazor によってコンポーネントのインスタンスが作成されるとき:
- コンポーネントのコンストラクターが呼び出されます。
@inject
ディレクティブ、または[Inject]
属性を介して、コンポーネントのコンストラクターに渡される DI サービスのコンストラクターが呼び出されます。
実行されたコンストラクターまたは任意の [Inject]
プロパティのセッターでエラーが発生すると、ハンドルされない例外になり、フレームワークによるコンポーネントのインスタンス化が停止されます。 アプリが回線経由で動作している場合、回線に障害が発生します。 コンストラクターのロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch
ステートメントを使用して、例外をトラップする必要があります。
ライフサイクル メソッド
コンポーネントの有効期間の間は、Blazor によってライフサイクル メソッドが呼び出されます。 いずれかのライフサイクル メソッドが同期的または非同期的に例外をスローした場合、例外は 回線にとって致命的です。 コンポーネントでライフサイクル メソッドのエラーに対処するには、エラー処理ロジックを追加します。
OnParametersSetAsync によって製品を取得するメソッドを呼び出す次の例では:
ProductRepository.GetProductByIdAsync
メソッドでスローされた例外はtry-catch
ステートメントによって処理されます。catch
ブロックの実行時には:loadFailed
がtrue
に設定されます。これがユーザーにエラー メッセージを表示するために使われます。- エラーがログに記録されます。
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product
<PageTitle>Product Details</PageTitle>
<h1>Product Details Example</h1>
@if (details != null)
{
<h2>@details.ProductName</h2>
<p>
@details.Description
<a href="@details.Url">Company Link</a>
</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}
@code {
private ProductDetail? details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
loadFailed = false;
// Reset details to null to display the loading indicator
details = null;
details = await Product.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
}
}
public class ProductDetail
{
public string? ProductName { get; set; }
public string? Description { get; set; }
public string? Url { get; set; }
}
/*
* Register the service in Program.cs:
* using static BlazorSample.Components.Pages.ProductDetails;
* builder.Services.AddScoped<IProductRepository, ProductRepository>();
*/
public interface IProductRepository
{
public Task<ProductDetail> GetProductByIdAsync(int id);
}
public class ProductRepository : IProductRepository
{
public Task<ProductDetail> GetProductByIdAsync(int id)
{
return Task.FromResult(
new ProductDetail()
{
ProductName = "Flowbee ",
Description = "The Revolutionary Haircutting System You've Come to Love!",
Url = "https://flowbee.com/"
});
}
}
}
@page "/product-details/{ProductId:int?}"
@inject ILogger<ProductDetails> Logger
@inject IProductRepository Product
<PageTitle>Product Details</PageTitle>
<h1>Product Details Example</h1>
@if (details != null)
{
<h2>@details.ProductName</h2>
<p>
@details.Description
<a href="@details.Url">Company Link</a>
</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}
@code {
private ProductDetail? details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
loadFailed = false;
// Reset details to null to display the loading indicator
details = null;
details = await Product.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
}
}
public class ProductDetail
{
public string? ProductName { get; set; }
public string? Description { get; set; }
public string? Url { get; set; }
}
/*
* Register the service in Program.cs:
* using static BlazorSample.Components.Pages.ProductDetails;
* builder.Services.AddScoped<IProductRepository, ProductRepository>();
*/
public interface IProductRepository
{
public Task<ProductDetail> GetProductByIdAsync(int id);
}
public class ProductRepository : IProductRepository
{
public Task<ProductDetail> GetProductByIdAsync(int id)
{
return Task.FromResult(
new ProductDetail()
{
ProductName = "Flowbee ",
Description = "The Revolutionary Haircutting System You've Come to Love!",
Url = "https://flowbee.com/"
});
}
}
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository
@if (details != null)
{
<h1>@details.ProductName</h1>
<p>@details.Description</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}
@code {
private ProductDetail? details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
loadFailed = false;
// Reset details to null to display the loading indicator
details = null;
details = await ProductRepository.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
}
}
public class ProductDetail
{
public string? ProductName { get; set; }
public string? Description { get; set; }
}
public interface IProductRepository
{
public Task<ProductDetail> GetProductByIdAsync(int id);
}
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository
@if (details != null)
{
<h1>@details.ProductName</h1>
<p>@details.Description</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}
@code {
private ProductDetail? details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
loadFailed = false;
// Reset details to null to display the loading indicator
details = null;
details = await ProductRepository.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
}
}
public class ProductDetail
{
public string? ProductName { get; set; }
public string? Description { get; set; }
}
public interface IProductRepository
{
public Task<ProductDetail> GetProductByIdAsync(int id);
}
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository
@if (details != null)
{
<h1>@details.ProductName</h1>
<p>@details.Description</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}
@code {
private ProductDetail details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
loadFailed = false;
// Reset details to null to display the loading indicator
details = null;
details = await ProductRepository.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
}
}
public class ProductDetail
{
public string ProductName { get; set; }
public string Description { get; set; }
}
public interface IProductRepository
{
public Task<ProductDetail> GetProductByIdAsync(int id);
}
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject ILogger<ProductDetails> Logger
@inject IProductRepository ProductRepository
@if (details != null)
{
<h1>@details.ProductName</h1>
<p>@details.Description</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}
@code {
private ProductDetail details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
loadFailed = false;
// Reset details to null to display the loading indicator
details = null;
details = await ProductRepository.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
}
}
public class ProductDetail
{
public string ProductName { get; set; }
public string Description { get; set; }
}
public interface IProductRepository
{
public Task<ProductDetail> GetProductByIdAsync(int id);
}
}
レンダリング ロジック
Razor コンポーネント ファイル (.razor
) 内の宣言マークアップは、BuildRenderTree という名前の C# メソッドにコンパイルされます。 コンポーネントがレンダリングされるときには、BuildRenderTree が実行されて、レンダリングされたコンポーネントの要素、テキスト、および子コンポーネントを記述するデータ構造が構築されます。
レンダリング ロジックは例外をスローすることがあります。 このシナリオの例は、@someObject.PropertyName
が評価されても @someObject
が null
であるときに発生しています。 回線上で動作する Blazor アプリの場合、レンダリング ロジックによってスローされる未処理の例外は、アプリの回線にとって致命的です。
レンダリング ロジックで NullReferenceException が発生しないようにするには、そのメンバーにアクセスする前に null
オブジェクトかどうかを調べます。 次の例では、person.Address
が null
の場合は 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>
}
上記のコードは、person
が null
でないことを前提としています。 多くの場合、コードの構造によって、コンポーネントがレンダリングされる時点でオブジェクトの存在が保証されます。 そのような場合は、レンダリング ロジックで null
かどうかを調べる必要はありません。 前の例では、コンポーネントがインスタンス化されるときに person
が作成されるため、person
が存在することが保証されている可能性があります。次にその例を示します。
@code {
private Person person = new();
...
}
イベント ハンドラー
クライアント側のコードでは、以下を使用して、イベント ハンドラーが作成されるときに C# コードの呼び出しをトリガーします。
@onclick
@onchange
- その他の
@on...
属性 @bind
これらのシナリオでは、イベント ハンドラー コードによって、ハンドルされない例外がスローされることがあります。
アプリが外部の理由で失敗する可能性のあるコードを呼び出す場合は、エラー処理とログ記録を含む try-catch
ステートメントを使用して、例外をトラップします。
イベント ハンドラーが、開発者コードによってトラップおよび処理されない、ハンドルされない例外をスローした場合 (たとえば、データベース クエリが失敗した):
- フレームワークによって例外がログされます。
- 回線を介して動作する Blazor アプリでは、例外はアプリの回線にとって致命的です。
コンポーネントの廃棄
たとえばユーザーが別のページに移動したため、コンポーネントが UI から削除されることがあります。 System.IDisposable を実装しているコンポーネントが UI から削除されると、フレームワークにより、コンポーネントの Dispose メソッドが呼び出されます。
コンポーネントの Dispose
メソッドが、回路上で動作する Blazor アプリで未処理の例外をスローした場合、その例外はアプリの回路にとって致命的です。
破棄のロジックによって例外がスローされる可能性がある場合、アプリでは、エラー処理とログ記録を含む try-catch
ステートメントを使用して、例外をトラップする必要があります。
コンポーネントの破棄について詳しくは、「ASP.NET Core Razor コンポーネントのライフサイクル」をご覧ください。
JavaScript 相互運用
IJSRuntime は Blazor フレームワークによって登録されます。 IJSRuntime.InvokeAsync を使用すると、.NET コードによって、ユーザーのブラウザーで JavaScript (JS) ランタイムの非同期呼び出しを行えます。
InvokeAsync を使用するエラー処理には、以下の条件が適用されます。
- InvokeAsync の呼び出しが同期的に失敗した場合は、.NET 例外が発生します。 たとえば、指定された引数をシリアル化できないため、InvokeAsync の呼び出しが失敗することがあります。 開発者コードで例外をキャッチする必要があります。 イベント ハンドラーまたはコンポーネントのライフサイクル メソッドのアプリ コードが、回線上で動作する Blazor アプリの例外を処理しない場合、結果として生じる例外はアプリの回線にとって致命的です。
- InvokeAsync の呼び出しが非同期に失敗した場合、.NET Task が失敗します。 たとえば、JS 側のコードが例外をスローしたり、
rejected
として完了したPromise
を返したりするために、InvokeAsync の呼び出しが失敗することがあります。 開発者コードで例外をキャッチする必要があります。await
演算子を使用する場合は、エラー処理とログ記録を含むtry-catch
ステートメントでメソッド呼び出しをラップすることを検討してください。 そうしないと、回線上で動作する Blazor アプリでコードに障害が発生すると、アプリの回線にとって致命的な未処理の例外が発生します。 - InvokeAsync の呼び出しは一定の期間内に完了する必要があります。そうでないと呼び出しがタイムアウトになります。既定のタイムアウト時間は 1 分間です。 タイムアウトにより、完了メッセージを送り返さないネットワーク接続や JS コードでの損失からコードを保護します。 呼び出しがタイムアウトになった場合、結果の System.Threading.Tasks は OperationCanceledException で失敗します。 ログ記録を使用して例外をトラップし、処理します。
同様に、JS コードを使用して、[JSInvokable]
属性によって示される .NET メソッドの呼び出しを開始できます。 ハンドルされない例外が、これらの .NET メソッドでスローされた場合:
- 回線を介して動作する Blazor アプリでは、例外はアプリの回線にとって致命的なものとして扱われません。
- JS 側の
Promise
は拒否されます。
.NET 側か、メソッド呼び出しの JS 側か、どちらでエラー処理コードを使用するかを選択できます。
詳細については、次の記事を参照してください。
- ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す
- ASP.NET Core Blazor で JavaScript 関数から .NET メソッドを呼び出す
プリレンダリング
Razor コンポーネントは既定で事前レンダリングされるため、レンダリングされた HTML マークアップはユーザーの最初の HTTP 要求の一部として返されます。
回線上で動作する Blazor アプリでは、プリレンダリングは次のように機能します。
- 同じページに含まれるすべての事前レンダリング コンポーネントに対する新しい回線を作成する。
- 初期 HTML を生成する。
- ユーザーのブラウザーが同じサーバーに戻る SignalR 接続を確立するまで、回線を
disconnected
として扱う。 接続が確立されると、回線でのインタラクティビティが再開され、コンポーネントの HTML マークアップが更新されます。
事前レンダリングされたクライアント側コンポーネントの場合、事前レンダリングは次のように機能します。
- 同じページに含まれるプリレンダリング済みコンポーネントすべてについて、サーバー上で最初の HTML を生成します。
- ブラウザーがアプリのコンパイル済みコードと .NET ランタイム (まだ読み込んでいない場合) をバックグラウンドで読み込んだ後、クライアントでコンポーネントを対話型にします。
ライフサイクル メソッドやレンダリング ロジックの実行中など、プリレンダリング中にコンポーネントからハンドルされない例外がスローされた場合:
- 回線を介して動作する Blazor アプリでは、例外は回線にとって致命的です。 事前にレンダリングされたクライアント側コンポーネントの場合、例外によりコンポーネントのレンダリングが妨げられます。
- その例外は、ComponentTagHelper の呼び出し履歴から破棄されます。
事前レンダリングが失敗する通常の状況では、コンポーネントのビルドとレンダリングを続行しても意味がありません。これは、動作中のコンポーネントはレンダリングできないためです。
事前レンダリング中に発生する可能性のあるエラーに耐えるには、例外をスローする可能性のあるコンポーネント内にエラー処理ロジックを配置する必要があります。 エラー処理とログ記録を含む try-catch
ステートメントを使用してください。 try-catch
ステートメント内に ComponentTagHelper をラップするのではなく、ComponentTagHelper によってレンダリングされるコンポーネント内にエラー処理ロジックを配置します。
高度なシナリオ
再帰的レンダリング
コンポーネントは、再帰的に入れ子にすることができます。 これは、再帰的なデータ構造を表現する場合に役立ちます。 たとえば TreeNode
コンポーネントで、ノードの子ごとにより多くの TreeNode
コンポーネントをレンダリングできます。
再帰的にレンダリングする場合は、無限の再帰となるようなコーディング パターンは回避します。
- 循環が含まれるデータ構造は再帰的にレンダリングしないでください。 たとえば、子にそれ自体が含まれるツリー ノードはレンダリングしないでください。
- 循環を含むひと続きのレイアウトは作成しないでください。 たとえば、レイアウトがそれ自体であるレイアウトは作成しないようにします。
- エンドユーザーが、悪意のあるデータ入力や JavaScript の相互運用呼び出しを通して、再帰による不変 (ルール) を犯さないようにします。
レンダリング中の無限ループ:
- レンダリング プロセスが永久に続行されるようになります。
- これは終了しないループを作成するのと同じです。
これらのシナリオでは、Blazor は失敗し、通常は次のことを試行します。
- オペレーティング システムで許されている限りの CPU 時間を無期限に消費します。
- メモリの量を無制限に消費します。 メモリを無制限に使用することは、すべての反復処理で、終了しないループによってコレクションにエントリが追加されるシナリオと同じです。
無限の再帰パターンを回避するには、再帰的なレンダリング コードに適切な停止条件が含まれるようにします。
カスタム レンダリング ツリーのロジック
ほとんどの Razor コンポーネントは、Razor コンポーネント ファイル (.razor
) として実装され、RenderTreeBuilder 上で動作して出力をレンダリングするロジックを生成するため、フレームワークによってコンパイルされます。 ただし、開発者は、手続き型の C# コードを使用して RenderTreeBuilder ロジックを手動で実装できます。 詳細については、「ASP.NET Core Blazor の高度なシナリオ (レンダー ツリーの構築)」を参照してください。
警告
手動のレンダー ツリー ビルダー ロジックの使用は、高度で安全ではないシナリオと考えられています。一般のコンポーネント開発には推奨されません。
RenderTreeBuilder コードが記述される場合は、開発者がコードの正確性を保証する必要があります。 たとえば、開発者は以下のことを確認する必要があります。
- OpenElement と CloseElement の呼び出しが正しく調整されている。
- 正しい場所にのみ属性が追加される。
手動レンダー ツリー ビルダーのロジックが正しくないと、クラッシュ、アプリまたはサーバーの応答の停止、セキュリティ脆弱性など、不特定の未定義の動作が発生するおそれがあります。
手動のレンダリング ツリー ビルダー ロジックは、アセンブリ コードや Microsoft Intermediate Language (MSIL) 命令を手動で記述するのと同じレベルの複雑さと、同じレベルの "危険性" を伴うことを考慮に入れてください。
その他のリソース
† クライアント側 Blazor アプリでログ記録に使用されるバックエンド ASP.NET Core Web API アプリに適用されます。
ASP.NET Core