Blazor のサービスクラスで例外ログを残すとき、「ユーザーが見ていたページの絶対URL(現在URL)」も一緒に記録したい場面は多いです。一方で HttpContext を参照しようとすると null になってしまい、思ったように取得できません。本記事ではその理由を整理しつつ、NavigationManager を使って安全に現在URLを取得する実装パターンを具体例付きで解説します。
サービスクラスで HttpContext が参照できないのは「Blazorの動き方」が原因
ASP.NET Core MVC や Razor Pages では、リクエスト処理中であれば HttpContext が存在し、HttpContext.Request から URL 情報(スキーム、ホスト、パス、クエリ)を取得できます。
しかし Blazor(特にインタラクティブ レンダリング)では、コンポーネントの実行タイミングが「HTTPリクエスト中」と一致しないことが多く、HttpContext が常に存在する前提で設計すると破綻しやすくなります。
レンダリング方式によって「HttpContextがある瞬間」と「ない瞬間」が混在する
Blazor は実行形態やレンダリング モードによって、処理の実体がサーバー側にあったりブラウザー側にあったりします。特に .NET 8 以降の Blazor Web App のように、静的レンダリングとインタラクティブが混在できる構成では「最初はサーバーで描画できるが、その後は対話処理が別経路になる」ケースが増えます。
| 状況 | HttpContext が期待できるか | 現在URL取得のおすすめ |
|---|---|---|
| 静的レンダリング(プリレンダリング)中 | 期待できる(ただし設計に依存) | HttpContext でも可だが、将来の混在を考えるなら NavigationManager を優先 |
| インタラクティブ(対話型)処理中 | null になりやすい / 参照できないことがある | NavigationManager(クライアント起点のURI情報) |
| Blazor WebAssembly(ブラウザー内実行) | そもそもサーバーの HttpContext は存在しない | NavigationManager |
| バックグラウンド処理(キュー / タイマー / HostedService) | 基本的に存在しない | 呼び出し元から URL を渡す、もしくは「URLなし」でも動く設計 |
つまり、「サービスクラスでいつでも HttpContext が取れる」と考えるのが落とし穴です。特に例外ログは UI イベントからも、バックグラウンドからも呼ばれがちなので、HttpContext 依存は早い段階で限界が来ます。
参考:静的レンダリングのルートコンポーネントでは HttpContext を扱えることがある
Blazor Web App などの構成では、静的レンダリング中に限って HttpContext をルートコンポーネントで受け取り、カスケードして子コンポーネントへ渡せる場合があります。ただし、これは「静的レンダリング中の一時的な値」であり、インタラクティブに切り替わった後も常に利用できるとは限りません。
ログ用途で HttpContext に寄せると、レンダリング方式の切り替えや将来の構成変更で再発しやすいので、現在URL取得は NavigationManager に統一するのが安全です。
Blazor で現在ページの絶対URLを取る定石は NavigationManager
Blazor で画面遷移や現在地(URI)を扱う中心的なサービスが NavigationManager です。コンポーネントだけでなく、DI 可能なクラス(スコープに注意)からも参照でき、現在ページの絶対URLは NavigationManager.Uri で取得できます。
| 手段 | 取得できるURL | Blazor WebAssembly | Blazor Server / Web App(Interactive) | おすすめ度 |
|---|---|---|---|---|
| HttpContext.Request | サーバー視点の要求URL | 不可 | 一部の瞬間のみ可(nullになり得る) | 低 |
| NavigationManager.Uri | ユーザーが見ている現在URL(絶対URL) | 可 | 可 | 高 |
最小実装:サービスに NavigationManager をDIして Uri を返す
まずは最もシンプルな実装です。サービス内で現在URLを取りたい場合、NavigationManager をコンストラクターインジェクションして Uri を参照します。
using Microsoft.AspNetCore.Components;
public class MyService
{
private readonly NavigationManager _navigationManager;
public MyService(NavigationManager navigationManager)
=> _navigationManager = navigationManager;
public string GetCurrentAbsoluteUrl()
=> _navigationManager.Uri; // 例: https://example.com/page?x=1
}
「絶対URL」「ベースURL」「相対パス」を使い分ける
ログの設計では、どの形式で残すかを決めておくと後が楽です。NavigationManager では次の情報が取れます。
| 取得したい情報 | 使うもの | 例 |
|---|---|---|
| 現在ページの絶対URL | NavigationManager.Uri | https://example.com/products?id=10 |
| アプリのベースURL | NavigationManager.BaseUri | https://example.com/ |
| アプリ基準の相対パス | NavigationManager.ToBaseRelativePath(...) | products?id=10 |
NavigationManager.Uri は「そのまま貼れば開ける」一方で長くなりやすい、ToBaseRelativePath は短いが「どの環境のURLか」を後から解決する必要がある、といったトレードオフがあります。
ログ用途でありがちな「URLの取り方」パターン
ログに残すURLは、実は目的によって最適形が異なります。例えば、問い合わせ時に再現用リンクとして使うならクエリを含めた方が便利ですが、個人情報やトークンがクエリに入る設計だと、そのまま保存するのは危険です。
| 目的 | 推奨するURL形式 | 理由 |
|---|---|---|
| 再現性を重視(問い合わせで同じ画面を開きたい) | 基本は NavigationManager.Uri(ただしクエリは要検討) | 状態を持つ画面はクエリが重要になることがある |
| 画面特定だけできればよい(統計・集計) | パスまで(クエリ除外) | 同じ画面がクエリで分岐しても集計が散らばりにくい |
| セキュリティを優先(トークン・メール等が混入し得る) | クエリをマスク/削除したURL | ログの二次漏えいを防ぐ |
クエリを除いた「ページURLだけ」を取りたい場合
NavigationManager.Uri を System.Uri に変換し、パス部分だけを取り出すと扱いやすくなります。
public string GetCurrentPageUrlWithoutQuery()
{
var uri = new Uri(_navigationManager.Uri);
return uri.GetLeftPart(UriPartial.Path); // 例: https://example.com/page
}
逆に「アプリのベースURLからの相対パスだけ」が欲しい場合は、NavigationManager.ToBaseRelativePath を使うと、ログの文字列が短くなり保守が楽になります。
public string GetCurrentRelativePath()
{
// 例: "page?x=1" のような形式(先頭の / は付かない)
return _navigationManager.ToBaseRelativePath(_navigationManager.Uri);
}
実務で効く設計:ログサービスを「URLが取れる時だけ付ける」形にする
ここからが運用上のポイントです。例外ログや監査ログは、UI 操作中だけでなく、バックグラウンドやAPI処理など「現在URLが存在しない状況」からも呼ばれる可能性があります。そこで、ログサービスは次のどちらかの設計に寄せると壊れにくくなります。
- ログサービス自身が
NavigationManagerからURLを取得する(UI起点の呼び出しが中心の場合) - 呼び出し元(コンポーネント等)がURLを渡し、ログサービスは「URLは任意」として扱う(混在が多い場合)
パターンA:ログサービスが NavigationManager からURLを取得する
UI 起点の例外が中心で、ログサービスの呼び出しが基本的にコンポーネント内に限定されるなら、このパターンが簡潔です。
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
public class ErrorLogService
{
private readonly NavigationManager _nav;
private readonly ILogger<ErrorLogService> _logger;
public ErrorLogService(NavigationManager nav, ILogger<ErrorLogService> logger)
{
_nav = nav;
_logger = logger;
}
public void LogError(Exception ex)
{
var url = _nav.Uri;
// 例:構造化ログでURLをプロパティとして残す
_logger.LogError(ex, "Unhandled error. url={Url}", url);
}
}
この形だと呼び出し側はシンプルですが、ログサービスのライフタイムには注意が必要です。
DIライフタイムの注意:Singleton にしない
Blazor Server / Blazor Web App(Interactive Server) では、NavigationManager は一般にスコープ(ユーザーごとの接続単位)に紐づきます。そのため、ログサービスを Singleton にしてしまうと、スコープ依存のサービスを注入できず例外になります。
| ログサービスの登録 | NavigationManager を注入 | 向いているケース |
|---|---|---|
| Scoped | 可能 | 「ユーザー操作中のログ」を中心に扱う |
| Transient | 可能 | 軽量なサービス。状態を持たず、その都度生成してよい |
| Singleton | 不可(基本的にNG) | アプリ全体で1つのインスタンスを共有したいが、現在URLのようなユーザー依存情報とは相性が悪い |
「どうしても Singleton なロガーを使いたい」場合は、次のパターンB(URLを引数で受け取る)に寄せるのが安全です。
パターンB:呼び出し元でURLを取得してログ関数に渡す
こちらはより堅牢です。ログサービスは「URLは任意(null許容)」とし、呼び出し元が渡せるときだけ渡します。バックグラウンド処理から呼ばれても設計上自然に扱えます。
using Microsoft.Extensions.Logging;
public class ErrorLogService
{
private readonly ILogger<ErrorLogService> _logger;
public ErrorLogService(ILogger<ErrorLogService> logger)
=> _logger = logger;
public void LogError(Exception ex, string? currentUrl = null)
{
// currentUrl が無い場合も想定しておく
_logger.LogError(ex, "Unhandled error. url={Url}", currentUrl ?? "(n/a)");
}
}
呼び出し側(コンポーネント)では次のように扱います。
@inject NavigationManager Nav
@inject ErrorLogService ErrorLog
@code {
private void OnClick()
{
try
{
// 何かの処理
}
catch (Exception ex)
{
ErrorLog.LogError(ex, Nav.Uri);
}
}
}
この設計のメリットは、ログサービス側が「URL取得に失敗する」こと自体を気にしなくてよい点です。ログを呼ぶ場所によって、渡せる情報だけを渡す形に統一できます。
ログにURLを残すときのセキュリティと運用の落とし穴
現在URLは非常に強い手がかりになりますが、同時に漏えいリスクも持ちます。特に次のようなパラメーターをクエリに含める実装は少なくありません。
- メールアドレスやユーザー名
- 検索キーワード(個人情報が混じる場合がある)
- ワンタイムトークン、招待コード、リセット用コード
問い合わせ対応のしやすさと安全性のバランスを取るため、ログ専用に「URLを正規化・マスクする」処理を用意しておくと安心です。
例:特定のクエリキーだけをマスクしてログに残す
ここではシンプルに、token や email のようなキーが含まれる場合に値を伏せる例を示します。要件に合わせて、マスク対象キーのリストを増やしてください。
public static string SanitizeUrlForLog(string absoluteUrl)
{
var uri = new Uri(absoluteUrl);
// クエリが無ければそのまま
if (string.IsNullOrEmpty(uri.Query))
return absoluteUrl;
// "a=1&b=2" 形式を手で分解(依存を増やしたくない場合の簡易実装)
var pairs = uri.Query.TrimStart('?')
.Split('&', StringSplitOptions.RemoveEmptyEntries);
var maskedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"token", "access_token", "email", "code"
};
var rebuilt = new List<string>();
foreach (var p in pairs)
{
var kv = p.Split('=', 2);
var key = Uri.UnescapeDataString(kv[0]);
var value = kv.Length > 1 ? Uri.UnescapeDataString(kv[1]) : "";
if (maskedKeys.Contains(key))
value = "*****";
rebuilt.Add($"{Uri.EscapeDataString(key)}={Uri.EscapeDataString(value)}");
}
var builder = new UriBuilder(uri)
{
Query = string.Join("&", rebuilt)
};
return builder.Uri.ToString();
}
ポイントは「ログ用の加工をサービス内に閉じ込める」ことです。開発者ごとに勝手なマスクを入れ始めると、ログの形が揃わず運用が崩れます。必ず共通関数として一本化し、レビュー対象にします。
よくある詰まりポイントと解決チェックリスト
最後に、「HttpContext が null」「URLが取れない」「DIで落ちる」といったありがちなトラブルを、原因と対処で整理します。
| 症状 | ありがちな原因 | 対処 |
|---|---|---|
サービス内で IHttpContextAccessor.HttpContext が null | インタラクティブ処理中、または WASM 実行で HttpContext が存在しない | NavigationManager.Uri を使う。必要なら「URLは引数で渡す」設計に変更 |
NavigationManager を注入したら例外(スコープ関連) | ログサービスを Singleton 登録している | ログサービスを Scoped/Transient にするか、URLを引数で渡す方式へ |
| ログに残ったURLが長すぎて読みづらい | クエリやフラグメントまで全部残している | パスのみを残す/ベース相対パスで残す/要件に応じて正規化する |
| ログに機密情報が混入した | トークン等がクエリに含まれている | URLマスク処理を共通化し、危険なキーは必ず伏せる |
| バックグラウンド処理からログを呼ぶとURLが取れない | そもそも「現在ページ」という概念が無い | URLは任意項目として扱い、無い前提でログを設計する |
まとめ
- Blazor では
HttpContextが常に使える前提ではなく、インタラクティブ レンダリングでは null になりやすい - 「ユーザーが見ている現在ページの絶対URL」を取るなら
NavigationManager.Uriが基本 - ログサービスはスコープに注意し、バックグラウンドから呼ばれても破綻しないよう「URLは任意」設計を検討する
- URLのクエリには機密情報が混入し得るため、ログ用に正規化・マスクする仕組みを用意する

コメント