Blazor 8.0でNavigateToがNavigationExceptionを投げる原因と解決策:起動時の言語切替をMiddlewareで安定化

Blazor 8.0(.NET 8)の言語切替で、起動時にURLとCookieのカルチャを同期させようとしてMainLayoutの初期化でNavigateTo()を呼ぶとNavigationExceptionが発生することがあります。原因と、確実に切り替える実装(Middleware優先)をまとめます。

目次

現象:初期化中のNavigateToでNavigationExceptionになる

Blazor アプリで多言語(カルチャ)を切り替える際、/Culture/Set?culture=xx-XX&redirectUri=/... のようなコントローラ(またはAPI)に一度リダイレクトして Cookie を設定し、元のページへ戻す実装は定番です。ところが Blazor 8.0(.NET 8)の構成、特に Blazor Server / Blazor Web App(SSR+Interactive) では、同じ遷移でも「どのタイミングで」「どの経路で」実行するかによって挙動が変わります。

やりたいこと画面上の言語セレクタMainLayout 初期化(OnInitializedAsync など)
コントローラへ遷移して Cookie を更新し、元のURLへ戻すだいたい期待どおり動くNavigationException が発生しやすい
URL(例:/en/…)と Cookie の言語が不一致なら起動時に自動で合わせたいユーザー操作なので問題が表面化しにくい初期レンダリング中に遷移しようとして破綻しやすい
URL を貼り付けて別言語ページへ移動したら即反映してほしいフルリロードなら反映しやすいコントローラに到達しない遷移だと 1回遅れて反映 することがある

結論から言うと、根っこは「Blazor が動き出してから(レンダリング中/回線確立後)に、サーバー側で Cookie を更新する前提のリダイレクト」を混ぜてしまうことです。Blazor の“クライアント側ナビゲーション”と、サーバーの“HTTPレスポンスで Cookie を書き換える”は、相性が悪い場面があります。

前提:/Culture/Set で Cookie を更新する「定番」パターン

まず、よくある構成を整理します。言語切替 UI は Blazor 側で、カルチャ確定(Cookie 書き込み)はサーバー側で行うイメージです。

カルチャを設定するコントローラ例

using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;

[Route("Culture")]
public class CultureController : Controller
{
[HttpGet("Set")]
public IActionResult Set([FromQuery] string culture, [FromQuery] string redirectUri = "/")
{
// サポート外の culture を弾く(例)
var supported = new[] { "ja-JP", "en-US" };
if (!Array.Exists(supported, c => c.Equals(culture, StringComparison.OrdinalIgnoreCase)))
{
culture = "ja-JP";
}


    var requestCulture = new RequestCulture(culture);

    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(requestCulture),
        new CookieOptions
        {
            Expires = DateTimeOffset.UtcNow.AddYears(1),
            IsEssential = true,
            Path = "/"
        });

    // オープンリダイレクト対策:ローカルURL以外は / へ
    if (!Url.IsLocalUrl(redirectUri))
    {
        redirectUri = "/";
    }

    return LocalRedirect(redirectUri);
}


}

Blazor 側から呼び出す例

@inject NavigationManager Nav

English
日本語

@code {
void ChangeCulture(string culture)
{
var current = Nav.ToBaseRelativePath(Nav.Uri);
var url = $"/Culture/Set?culture={Uri.EscapeDataString(culture)}&redirectUri=/{Uri.EscapeDataString(current)}";
Nav.NavigateTo(url, forceLoad: true); // 後述:forceLoad が重要
}
}

ポイントは、Cookie を書き換えるには HTTPレスポンス が必要だということです。ブラウザが「コントローラへ通常のリクエスト」を投げ、そのレスポンスで Set-Cookie を受け取って初めて、次のリクエストから新しいカルチャが使えるようになります。

原因:Blazor のナビゲーションと HTTP の境界がズレる

NavigateTo は「描画を中断する」ために例外を使うことがある

Blazor のコンポーネントは、初期化(OnInitialized[Async])→レンダリング→イベント…という流れで動きます。このレンダリングの途中でナビゲーション(別ページへの遷移)を要求すると、フレームワークは「今の描画を中止して遷移を優先する」ために内部的に例外を投げて処理を打ち切ることがあります。それが NavigationException として表面化します。

とくに Layout(MainLayout)や Router 周辺は、アプリ起動時に必ず通るため影響が大きく、初期化中に強引に NavigateTo() を叩くと「まだ描画が安定していないのに遷移をねじ込む」形になりがちです。

Blazor Server では「ページ遷移=常にHTTPリクエスト」ではない

Blazor Server は SignalR の“回路(circuit)”上で UI を更新します。画面遷移も多くの場合、ブラウザが新しいページを取りに行くのではなく、クライアント側でルーティングが切り替わり、回路内でコンポーネントが差し替わる形で完結します。

項目フルページ遷移(通常のHTTP)Blazor のクライアント側ナビゲーション
コントローラへ到達するか到達する到達しない(ルーティング内完結の場合がある)
Cookie を書き換えるタイミングレスポンスで確実に Set-Cookieそもそもレスポンスが無い/弱いので前提が崩れる
見た目ブラウザが再読み込み(ちらつくことがある)SPA のようにスムーズ

つまり「コントローラでCookieをセットすること」を前提にしているのに、遷移経路が Blazor 側で完結してしまうと、Cookie 更新が期待どおり発生しません。これが「URL を変えたのに表示言語がすぐ反映されない」「次の遷移で反映される」などの体感につながります。

URL を貼り付けたときに“1回遅れる”のはなぜ?

典型例は次のような状態です。

  • URL は /en/... なのに、Cookie には ja-JP が残っている
  • RequestLocalization(や独自ロジック)は Cookie を優先してカルチャを決めている
  • Blazor の初期描画は Cookie のカルチャで進む
  • その後「合わせよう」として Blazor 側で NavigateTo を行うが、コントローラに届かず Cookie が更新されない

結果として、次の“フルページリクエストが発生したとき”にしか Cookie の状態が変わらず、言語が 1 回遅れて反映されたように見えます。

解決策(推奨):Blazor が起動する前に Middleware で判定して Response.Redirect する

一番事故が少ないのは、最初の HTTP リクエスト段階(=Blazor の回路が始まる前)に整合性を取ることです。起動時に URL と Cookie の言語が食い違うなら、そこで一度だけ /Culture/Set へ Response.Redirect し、Cookie を確定させてから Blazor を動かします。

全体像:責務をサーバー側に寄せる

レイヤ役割メリット
Middleware(Blazor 起動前)URL と Cookie の不一致を検出し、必要なら /Culture/Set へリダイレクト初期化中の NavigateTo を不要にし、例外や二重遷移を避けられる
Controller / APICookie を確実に Set-Cookie し、元URLへ戻すCookie 更新が HTTP の責務として明確になる
Blazor コンポーネント「確定したカルチャ」で描画するUI は純粋になり、ライフサイクル依存のバグが減る

実装例:URLセグメントと現在カルチャを比較してリダイレクト

例として、URL の先頭セグメントが /en/ または /ja/ のときだけ、Cookie のカルチャと揃えるケースを示します。URL 直打ち・ブックマーク・外部リンク流入でも安定して動きます。

using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Options;
using System.Globalization;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddLocalization();

var supportedCultures = new[] { "ja-JP", "en-US" };
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
    options.SetDefaultCulture("ja-JP")
           .AddSupportedCultures(supportedCultures)
           .AddSupportedUICultures(supportedCultures);

    // Cookie を主要な判定材料にする(一般的な構成)
    options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider());
});

builder.Services.AddControllers();
builder.Services.AddRazorComponents(); // Blazor Web App の場合
// builder.Services.AddServerSideBlazor(); // Blazor Server の場合

var app = builder.Build();

// 先に RequestLocalization を有効化(Cookie などから CultureInfo が決まる)
var locOptions = app.Services.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value;
app.UseRequestLocalization(locOptions);

// URL と Cookie がズレているなら、Blazor より前にリダイレクトして揃える
app.Use(async (context, next) =>
{
    // /Culture/Set 自体は対象外(リダイレクトループ防止)
    if (context.Request.Path.StartsWithSegments("/Culture/Set", StringComparison.OrdinalIgnoreCase))
    {
        await next();
        return;
    }

    // 先頭セグメントから言語を拾う(例:/en/products, /ja/products)
    var path = context.Request.Path.Value ?? "";
    var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
    var first = segments.Length > 0 ? segments[0] : null;

    // URL セグメントとカルチャ名の対応表
    var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        ["ja"] = "ja-JP",
        ["en"] = "en-US"
    };

    if (first is not null && map.TryGetValue(first, out var urlCulture))
    {
        // RequestLocalization により、ここでは CurrentCulture が確定している
        var currentCulture = CultureInfo.CurrentCulture.Name;

        if (!string.Equals(currentCulture, urlCulture, StringComparison.OrdinalIgnoreCase))
        {
            // 今見ている URL に戻す(クエリ文字列も含める)
            var returnUrl = $"{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}";
            var redirect = $"/Culture/Set?culture={Uri.EscapeDataString(urlCulture)}&redirectUri={Uri.EscapeDataString(returnUrl)}";

            context.Response.Redirect(redirect, permanent: false);
            return;
        }
    }

    await next();
});

// 以降は通常のパイプライン(コントローラ、Blazor)
app.MapControllers();

// app.MapRazorComponents<App>().AddInteractiveServerRenderMode(); など
app.Run();

この方式が強い理由

  • Blazor のライフサイクルに依存しない:OnInitializedAsync の早すぎるタイミングで無理やり遷移しない
  • Cookie 更新が必ず HTTP のレスポンスで起きる:Set-Cookie が不発になりにくい
  • 初回から正しい言語で描画される:一度だけ 302 を挟むが、UI の整合性は高い

運用でハマりやすいポイント

落とし穴症状対策
リダイレクトループ/Culture/Set にも判定が走って無限 302/Culture/Set を middleware 判定から除外する
redirectUri のオープンリダイレクト外部サイトへ飛ばせてしまうコントローラ側で Url.IsLocalUrl を必ずチェック
PathBase(サブディレクトリ配下)戻り先URLが壊れるcontext.Request.PathBase を含めて returnUrl を作る
サポート外カルチャ例外や想定外の表示supportedCultures に無い場合は既定言語へフォールバック

解決策:どうしても Blazor 側から叩くなら「フルページ遷移」にする

画面上の言語セレクタのように、Blazor 側から /Culture/Set を叩く必要がある場合は、Blazor ルーティングではなくブラウザのフルリロードを伴う遷移に寄せます。具体的には NavigateTo(url, forceLoad: true) を使います。

初期化タイミングで呼ぶのではなく、レンダリング完了後に1回だけ実行する

Layout の初期化(OnInitialized/OnInitializedAsync)で遷移をかけると例外が出やすいので、少なくとも「描画が一度終わってから」実行します。Blazor Web App(SSR+Interactive)や Server では、OnAfterRenderAsync の firstRender を使うのが定番です。

@inject NavigationManager Nav

@code {
    bool _redirecting;

    protected override void OnAfterRender(bool firstRender)
    {
        if (!firstRender || _redirecting)
        {
            return;
        }

        // 例:URL の /en と Cookie(現在のカルチャ)がズレているか判定
        var currentCulture = System.Globalization.CultureInfo.CurrentCulture.Name;
        var isEnUrl = Nav.ToBaseRelativePath(Nav.Uri).StartsWith("en/", StringComparison.OrdinalIgnoreCase);

        if (isEnUrl && !string.Equals(currentCulture, "en-US", StringComparison.OrdinalIgnoreCase))
        {
            _redirecting = true;

            var current = Nav.ToBaseRelativePath(Nav.Uri);
            var url = $"/Culture/Set?culture=en-US&redirectUri=/{Uri.EscapeDataString(current)}";

            // forceLoad: true で必ずコントローラへ HTTP リクエストさせる
            Nav.NavigateTo(url, forceLoad: true);
        }
    }
}

この方法は動きますが、次のデメリットもあります。

  • 初回描画 → フルリロード → 再描画 となり、ちらつきや二重ロードが起きやすい
  • 判定を誤ると、戻り先と判定条件が噛み合わず無限リロードになりやすい
  • 根本(初回から正しいカルチャで描画)より後追いになる

そのため、起動時の整合性は Middleware 側で片付け、Blazor 側はユーザー操作の切替だけに使う、という分担が一番安定します。

設計の要点:カルチャを確定させるのは「Blazor 起動前」が原則

多言語対応で事故を減らすコツは、「どこでカルチャを決めるか」を最初に固定することです。Blazor のコンポーネントが描画を始めてからカルチャを変えようとすると、レンダリング・状態・ルーティングが絡み合い、今回のような例外や遅延が起きます。

よく使う“正(ソース・オブ・トゥルース)”の決め方

正にするもの向いているケース実装の軸
URL(/en/… のようなセグメント)言語ごとにURLを共有したい、SEO を強く意識するURL からカルチャを決め、Cookie を追従させる(今回の middleware 方式)
Cookieユーザーの好みを優先したい、URL は言語を持たせたくないCookie を優先し、必要なら /ja → /en のように URL 側をリダイレクト
Accept-Language初回訪問時だけ自動判定し、以降は Cookie を優先初回の既定設定を決める補助として使う(恒久の正にはしない)

「URL と Cookie を常に同期させる」なら、最初のリクエストで一度だけ合わせる

URL セグメントと Cookie のカルチャが食い違うケースは、起動時に 1 回だけ 302 を挟んで整合させるのが最も安定します。以降は Blazor 側がどれだけクライアント内で遷移しても、カルチャの前提は崩れにくくなります。

チェックリスト:この順番で見直すと早い

チェック項目確認ポイント目安
RequestLocalization の設定SupportedCultures / SupportedUICultures / DefaultCulture が意図通りかカルチャがブレるなら最優先
Cookie の書き込みCookieRequestCultureProvider.DefaultCookieName で書いているか、Path は / か他ページで反映されない原因になりやすい
Blazor 側の遷移コントローラに行く必要がある遷移は forceLoad: true かコントローラが呼ばれないならここ
起動時の整合性URL と Cookie がズレたとき、Blazor 起動前に揃えているかNavigationException や 1回遅れの主因
リダイレクトループ対策/Culture/Set を除外しているか、redirectUri がローカルか本番事故を防ぐ

よくある疑問

NavigationException は「バグ」?

多くの場合、NavigationException 自体は Blazor が内部で描画を中断するために使う仕組みです。問題は例外そのものではなく、初期化中にナビゲーションを強要し、レンダリングの途中で処理が切れてしまう設計にあります。起動時の言語確定を middleware に逃がすと、例外に悩まされにくくなります。

Blazor WebAssembly でも同じ?

WebAssembly でも「SPA 内遷移」と「HTTP リクエスト」が混ざると似た問題が起きます。ただし Hosted 構成(ASP.NET Core で API を持つ)か、静的ホスティングかで条件が変わります。いずれにせよ、Cookie を更新したいなら forceLoad でフルページ遷移、起動時の整合性はサーバー側で先に整える、という方針が共通して効きます。

URL セグメントからカルチャを読むなら、/Culture/Set を経由しなくてもいい?

可能です。URL セグメントを読む RequestCultureProvider を自作し、URL を正として毎リクエストでカルチャを決めれば、Cookie 更新自体が不要になります。ただし「ユーザーが次回アクセスしたときに URL に言語が無くても前回の好みを使いたい」などの要件があるなら、Cookie 併用の方が扱いやすいことも多いです。要件に合わせて、URL と Cookie のどちらを正にするかを決めてください。

まとめ:言語切替の“正しい場所”をずらすだけで安定する

MainLayout 初期化中に NavigateTo してカルチャを直そうとすると、Blazor のレンダリングとナビゲーションが衝突し、NavigationException や「反映が1回遅れる」挙動が出やすくなります。対策はシンプルで、Blazor が動き出す前の middleware 段階で URL と Cookie を判定し、必要なら Response.Redirect で /Culture/Set に回すことです。

どうしても Blazor 側から叩く場合は forceLoad: true でフルページ遷移に寄せ、呼ぶタイミングは OnAfterRender[Async] の firstRender に限定します。責務の置き場所を整理するだけで、言語切替の挙動は驚くほど安定します。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次