🍣

.NET 8 での Blazor で ErrorBoundary を使ったエラーハンドリングの注意点

2024/06/03に公開

以前にも以下のような似たような記事を書いたことがあります。

https://zenn.dev/microsoft/articles/aspnetcore-blazor-dotnet8-errorhandling

この記事を書いた時は .NET 8 も出て日が浅かったこともあり ErrorBoundary コンポーネントを使ったエラーハンドリングの上手い方法が思いつかなかったため、気合で頑張るみたいな方法を紹介しました。要は例外の出る可能性のあるメソッドで全部 try catch しようねという感じです。

もうちょっといい方法

リリースされて半年以上もたつとドキュメントも色々整備・更新されてきていて、レンダリングモードのページではグローバルで SSR/Interactive を切り替えるような方法も紹介されています。

以下のドキュメントの「グローバル対話型アプリの静的 SSR ページ」のセクションです。

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0#static-ssr-pages-in-a-globally-interactive-app

こうするとレイアウトファイルから一貫したレンダリングモードを使いつつ、静的 SSR や InteractiveServer/WASM/Auto の切り替えが出来るようになります。つまりレイアウトファイルに ErrorBoundary を配置しても、ちゃんとエラーが補足されるようになります。

やってみよう

Blazor Web App のプロジェクトで Interactive render mode を Auto にして Interactivity location は Global にしてプロジェクトを作成します。

App.razor ファイルを以下のようにしてページに設定されているレンダリングモードを Routes コンポーネントと HeadOutlet コンポーネントに設定するようにします。

App.razor
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="BlazorApp3.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet @rendermode="PageRenderMode" />
</head>

<body>
    <Routes @rendermode="PageRenderMode" />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? PageRenderMode =>
        HttpContext.GetEndpoint()?.Metadata.GetMetadata<RenderModeAttribute>()?.Mode;
}

そしてサーバーサイドのプロジェクトに Components/Layout/StaticSsrLayout.razor を作成して以下のようにします。
CascadingParameterHttpContext を取得して、それが null の場合は NavigationManager.Refresh を呼び出してリロードするようにしています。
これは CascadingParameter で渡される HttpContextInteractiveServer などのインタラクティブなレンダリングモードの場合は null になるため、その際にページをリフレッシュすると、結果として現在のページのレンダリングモードに合わせて画面全体が再レンダリングされるといった挙動になります。

StaticSsrLayout.razor
@using BlazorApp3.Client.Layout
@inherits LayoutComponentBase
@layout MainLayout
@inject NavigationManager NavigationManager

@if (HttpContext is null)
{
    <p>Loading...</p>
}
else
{
    @Body
}

@code {
    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    protected override void OnParametersSet()
    {
        if (HttpContext is null)
        {
            NavigationManager.Refresh(forceReload: true);
        }
    }
}

そして、Static SSR を適用する予定のページをサーバーサイドのプロジェクトに置いて、このレイアウトを適用するようにします。プロジェクトテンプレートで作成されるサンプルページの Home.razorWeather.razor が今回の対象になります。

Home.razor のコードは以下のようになります。Weather.razor も同じようにサーバーサイドのプロジェクトに移動してレイアウトを適用しています。

Home.razor
@page "/"
@using BlazorApp3.Components.Layout
@layout StaticSsrLayout

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

そして MainLayout.razor に以下のように ErrorBoundary を配置します。

MainLayout.razor
@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

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

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

@code {
    private ErrorBoundary? _errorBoundary;
    protected override void OnParametersSet()
    {
        _errorBoundary?.Recover();
    }
}

これで Home.razorCounter.razorWeather.razor で例外が発生しても、ErrorBoundary が補足してエラー画面を表示するようになります。
試しに Counter.razorWeather.razor でエラーを適当に発生させるようなコードを仕込んだ感じ以下のようにちゃんとレイアウト部分で設定している ErrorBoundary が補足してエラー画面を表示してくれます。

制限事項

ちゃんと動いていそうですが制限事項があります。
この方式を使った時にはアプリケーション内の全ページで一貫したレンダリングモードを設定する必要があります。

例えば InteractiveServer のページから InteractiveWebAssembly に遷移をするとエラーになります。これはレイアウト部分が InteractiveServer でレンダリングされている所に InteractiveWebAssembly のページがレンダリングされるためです。

ここら辺、うまいやり方があれば教えてください。

まとめ

ということで Static SSR と Interactive render mode の中から 1 種類の 2 つが混在するアプリで ErrorBoundary を使ってエラーハンドリングをする方法を紹介しました。
これなら、全メソッドに try catch するよりも楽にグローバルでエラーハンドリングをすることが出来るので、個人的にはこの方法を使うかなと思います。

InteractiveServerInteractiveWebAssembly 間の画面遷移が出来れば完璧だったんですが…。

Microsoft (有志)

Discussion