はじめに
本記事は「例外とログを深く考える」シリーズの第3回です。
前回までは、Exception.Data を活用してコンテキスト情報を伝播させる設計思想を解説しました。今回はその実践編として、Blazor(UI層) / ビジネス層 / インフラ層 の3層アーキテクチャを用いた具体的な実装例を紹介します。
下層で発生した例外が上層へ伝播するにつれ、その層が持つ「ビジネス知識」や「画面のコンテキスト」が Exception.Data にシームレスに付加されていく例外ハンドリングを解説します。
アーキテクチャの概要と課題
すべてのメソッドで try-catch を記述して情報を付加するのは、ボイラープレートコードを量産し、コードの可読性を著しく低下させます。
本稿では、「層が変わる境界(バウンダリ)」 に絞って例外をインターセプトし、その層固有の文脈情報を Exception.Data にカプセル化して上位に引き渡すアプローチをとります。
実装コード例
以下に、.NET 10 / C# 14 に準拠した3層アーキテクチャにおける例外ハンドリングの実装例を示します。
1. インフラ層(Infrastructure Layer)
外部APIやデータベースと通信する層です。低レベルな接続情報を付加します。
namespace App.Infrastructure;
public interface IUserRepository
{
Task<string> GetUserDataAsync(string userId);
}
public class UserRepository : IUserRepository
{
private readonly HttpClient _httpClient;
public UserRepository(HttpClient httpClient) => _httpClient = httpClient;
public async Task<string> GetUserDataAsync(string userId)
{
try
{
// .NET 10環境を想定した通信
var response = await _httpClient.GetAsync($"api/users/{userId}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
// インフラ層のバウンダリで、低レイヤのコンテキストを付加
ex.Data["Infra.TargetUrl"] = _httpClient.BaseAddress?.ToString();
ex.Data["Infra.Endpoint"] = $"api/users/{userId}";
ex.Data["Infra.StatusCode"] = ex.StatusCode?.ToString() ?? "Unknown";
throw; // 元のスタックトレースを維持して再スロー
}
}
}
2. ビジネス層(Business / Application Layer)
業務ロジックを司る層です。インフラ層から上がってきた例外に、「どの業務ユースケースで、誰を対象にしていたか」というビジネス知識を付加します。
namespace App.Business;
using App.Infrastructure;
public interface IUserService
{
Task<UserProfile> GetProfileAsync(string userId, string tenantId);
}
public record UserProfile(string Id, string RawData);
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository) => _userRepository = userRepository;
public async Task<UserProfile> GetProfileAsync(string userId, string tenantId)
{
try
{
// インフラ層の呼び出し
var rawData = await _userRepository.GetUserDataAsync(userId);
return new UserProfile(userId, rawData);
}
catch (Exception ex)
{
// ビジネス層のバウンダリで、業務文脈(ドメイン知識)を付加
ex.Data["Business.UseCase"] = "FetchUserProfile";
ex.Data["Business.TargetUserId"] = userId;
ex.Data["Business.TenantId"] = tenantId;
ex.Data["Business.Timestamp"] = DateTimeOffset.UtcNow.ToString("o");
throw;
}
}
}
3. UI層(UI Layer - Blazor)
ユーザーとの接点です。try-catch をコンポーネント内で直接使用し、画面特有の情報(画面URLや操作ユーザー)を Exception.Data に付加してログへ出力します。
※ErrorBoundaryなども使用できますが今回はデモのため簡易的に実装します。
Blazorコンポーネント(UserProfilePage.razor)
@page "/user-profile"
@using App.Business
@inject IUserService UserService
@inject ILogger<UserProfilePage> Logger
@inject NavigationManager Navigation
<PageTitle>ユーザープロフィール</PageTitle>
@if (_hasError)
{
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">エラーが発生しました</h4>
<p>申し訳ありません。プロフィールの取得中に問題が発生しました。システム管理者に連絡してください。</p>
</div>
}
else if (_profile is null)
{
<p><em>Loading...</em></p>
}
else
{
<div class="card">
<div class="card-body">
<h5 class="card-title">ユーザーID: @_profile.Id</h5>
<p class="card-text">@_profile.RawData</p>
</div>
</div>
}
@code {
private UserProfile? _profile;
private bool _hasError;
protected override async Task OnInitializedAsync()
{
try
{
_profile = await UserService.GetProfileAsync("user-001", "tenant-alpha");
}
catch (Exception ex)
{
// UI層のコンテキスト(画面URL、操作ユーザー)を付加
ex.Data["UI.CurrentUrl"] = Navigation.Uri;
ex.Data["UI.Component"] = nameof(UserProfilePage);
// 実際には AuthenticationStateProvider などから取得する
ex.Data["UI.OperatorId"] = currentUser?.Identity?.Name ?? "anonymous";
// Exception.Data の内容を構造化ログへ流し込む
var scope = new Dictionary<string, object>();
foreach (System.Collections.DictionaryEntry entry in ex.Data)
{
if (entry.Key is string key)
scope[key] = entry.Value ?? "null";
}
using (Logger.BeginScope(scope))
{
Logger.LogError(ex, "プロフィール取得中に例外が発生しました。メッセージ: {Message}", ex.Message);
}
_hasError = true;
}
}
}
出力される構造化ログ(JSONイメージ)
この構成により、Azure Application Insights (Log Analytics) には以下のような、すべての層のコンテキストが統合された強力な構造化ログが出力されます。
{
"EventTime": "2026-05-17T19:38:00.1234567Z",
"Exception": "System.Net.Http.HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error).",
"Message": "アプリケーションの境界で例外を捕捉しました。メッセージ: Response status code does not indicate success: 500...",
"CustomDimensions": {
"Infra.TargetUrl": "https://api.internal.example.com/",
"Infra.Endpoint": "api/users/user-001",
"Infra.StatusCode": "500",
"Business.UseCase": "FetchUserProfile",
"Business.TargetUserId": "user-001",
"Business.TenantId": "tenant-alpha",
"Business.Timestamp": "2026-05-17T19:38:00.0000000+00:00",
"UI.CurrentUrl": "https://localhost:7001/user-profile",
"UI.Component": "UserProfilePage",
"UI.OperatorId": "user_suzuki_ichro_999"
}
}
まとめ:このパターンのメリット
-
コードの疎結合化 : 各層のメソッドは、自分自身の関心事(ビジネスロジックやインフラ通信)のコンテキストを
Exception.Dataに詰めるだけで済みます。例外オブジェクトをカスタム例外クラス(例:UserServiceException)でラップして再スローする必要がありません。 -
圧倒的な調査効率 : ログポータル(Log Analytics)で
CustomDimensions["Business.TenantId"] == "tenant-alpha"やCustomDimensions["Infra.StatusCode"] == "500"といったクエリで、UIからインフラまでを串刺しにしたフィルタリングが可能になります。 - 堅牢なセキュリティ : ユーザー画面には安全な固定メッセージのみを提示し、内部の機密なシステム構造(URLやパラメータ)を完全に隠蔽できます。
層の境界を意識した例外ハンドリングを取り入れ、エンタープライズに耐えうる堅牢なプロダクトを構築しましょう。
この記事が皆様のコーディングライフの助けになれば幸いです。
参考