昨日ユニットテストについて話しましたが、時間の都合で完全にテストできないこともあります。そうすると、特定のComponentでエラーが発生しただけでシステム全体がクラッシュする可能性があります(下図参照)。なぜならBlazor ServerはServer上でcircuit(フロー)を確立し、未処理のエラーがあるとServerはセキュリティ上の問題を避けるためにcircuitを終了させるからです。

毎回エラーが発生するたびにシステム全体を停止させたくないし、すべてのメソッドをtry…catch…で包んですべてのエラーを処理するのも避けたい。そこで.NET 6で新しく導入されたComponent <ErrorBoundary>を使います。
Reactというフロントエンドフレームワークにも同様のErrorBoundaryの概念があり、任意のComponentで発生したエラーをキャッチして、デフォルトのUIページを表示します。エラーが発生すると、そのComponentにエラーを限定し、他のComponentは機能を維持します。
Blazorもこの概念を借用していますが、Blazorチームはこれですべての可能な例外状況をキャッチできるわけではないと説明しています。また、<ErrorBoundary>はグローバルエラーハンドリングを目的としたものではなく、それはILoggerの役割です。<ErrorBoundary>の主な目的はレンダリングやライフサイクルメソッド(OnInitializedAsync、OnParametersSetAsync)で発生するエラーを処理することです。さらに、多くの潜在的なリスクもあります。例えば、開発者が<ErrorBoundary>ですべてのエラーを処理できると思い込む、開発者が異なるエラーに対してカスタマイズされたUIを作成する必要がないと感じるため、複数の異なるエラーが発生したときに混乱する、同じ種類の<ErrorBoundary>が同時に多数発生した際に、開発者がそのエラーごとにログを記録しようとしてログが詰まる、などです。そのため、これをエラー処理の最後の手段と見なしてはいけません。
上記の説明は少々恐ろしいですが、<ErrorBoundary>をシンプルなページロジックの処理に使うのは問題ありません。早速試してみましょう。
(注:Visual Studio 2022を直接使用してプロジェクトを作成した方は、次の手順を省略できます)
筆者はVisual Studio 2019を使っているため、.NET 6はVisual Studio 2022でしか実行できません。そこでVisual Studio 2022をダウンロードし、BlazorPracticeソリューションを開き、BlazorServerとBlazorServerMsTestの両プロジェクトの<TargetFramework>をnet6.0に変更します。

次に、wwwroot/css/site.cssに以下のクラスを追加します。<ErrorBoundary>はblazor-error-boundaryクラスを持つ<div>を生成します。
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
次に MainLayout.razor で <ErrorBoundary> を使って @Body をラップします。最後に BlogBase.razor.cs の LoadData() でエラーをスローするコードを追加します。

private async Task LoadData()
{
…
throw new Exception("これはテスト用エラーメッセージです");
}
サイトを開いてみると、今回は下に表示されていた黄色い例外エラー情報が表示されず、代わりにデフォルトのUIが表示されます。

ただし、どんなエラーでも同じメッセージが表示されるのは、ユーザーにとって親切とは言えません。そこで <ChildContent> と <ErrorContent> を使ってカスタマイズしましょう。これらは実質 RenderFragment であり、任意の HTMLタグ や Component を入れられます。

<ErrorBoundary>
<ChildContent> @Body </ChildContent>
<ErrorContent>
<p>大変申し訳ございません。現在不明なエラーが発生しております。管理者にお問い合わせください。</p>
</ErrorContent>
</ErrorBoundary>
しかし、この状態で Roles や Users に切り替えると、赤いエラー情報がページに残ったままになります。これは <ErrorBoundary> がエラーを検出するとそのまま表示を続けるためです。この場合は Recover() メソッドを呼び出します。このメソッドはエラー数を 0にリセット し、StateHasChanged() を呼び出して各Componentに状態が変わったことを通知します。これによりComponentが 再レンダリング されます。@ref を使って対象の <ErrorBoundary> を参照しないと Recover() は実行されないので注意してください。

…
<div class="content px-4">
<ErrorBoundary @ref="errorBoundary">
<ChildContent> @Body </ChildContent>
<ErrorContent>
<p>大変申し訳ございません。現在不明なエラーが発生しております。管理者にお問い合わせください。</p>
</ErrorContent>
</ErrorBoundary>
</div>
… @code { private ErrorBoundary errorBoundary; protected override void
OnParametersSet() { errorBoundary?.Recover(); } }
また、<ErrorBoundary> には変数 MaximumErrorCount があり、デフォルトは100です。MaximumErrorCount が指定数を超えるとシステムがクラッシュします。

感想
筆者はIT鉄人戦に申し込んだとき、単にプロジェクトの感想を記録したいと思っていました。完走できるか不安でしたが、実際に17日目に忘れてしまい、このような初歩的なミスを犯したことを恥ずかしく思います。その後は失敗したことで少し手抜きをしてしまいました。この姿勢は良くありません。いくつかの記事を統合して、新しいテーマで公開しようと思います。
完走はできませんでしたが、感想を記録することでいくつかの学びを得ました。この1年半、仕事で感想を書くときも断片的な記録で、最初から最後までしっかり書くことはありませんでした。今回の鉄人戦では詳しく書くために、多くの資料を何度も調べ直しました。これこそ正しい感想の書き方であり、来年の鉄人戦ではさらに成長したいと思います。
引用:
- Unhandled Exceptions in Blazor Server with Error Boundaries
- Blazor "Error boundaries" design proposal #30940
- Blazor .NET 6 - Error Boundaries - Custom UI for Errors
- How To Get .NET 6 in Visual Studio 2019
(注:筆者はこの動画の手順に従ってもVisual Studio 2019で.NET 6に切り替えられませんでした。他の方法をご存知の方はお知らせください。) Ref:
(注:Microsoft公式ダウンロードサイトでは.NET 6はVisual Studio 2019 SDKをサポートしていないと明記されています。上の動画の作者がどうやって実現したのかは不明です。)