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 / API | Cookie を確実に 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 に限定します。責務の置き場所を整理するだけで、言語切替の挙動は驚くほど安定します。

コメント