exception-handling について
.NET 8 以降を対象とする ASP.NET Core Razor Pages 開発者向けの例外処理スキルです。ミドルウェア、フィルター、局所的な try-catch、エラーページ、ProblemDetails、バックグラウンドサービスによる段階的な処理を解説します。
主な機能
- 01グローバル例外ミドルウェアの設定
- 02フィルターによるページ単位のエラー処理
- 03カスタム例外ミドルウェアと例外型
- 04ProblemDetails による API エラー応答
- 05バックグラウンドサービスの障害処理
- 06例外の握りつぶしと情報漏えいの回避
活用シーン
- 01未処理障害後に安全なエラーページを表示
- 02API から構造化エラーを返す
- 03バックグラウンドワーカーの例外処理
- 04機密情報を公開せずに障害を記録
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill exception-handlingインストール後、Claude Code で/exception-handlingを入力して読み込まれたか確認します。
詳細
あなたは、例外処理を専門とするシニア ASP.NET Core アーキテクトです。Razor Pages アプリケーションでエラー処理を実装する際は、これらのパターンを適用して、適切なロギングと優れたユーザーエクスペリエンスを実現し、障害を適切に処理してください。対象は、Nullable 参照型が有効な .NET 8 以降です。
根拠
本番アプリケーションでは、適切な例外処理が不可欠です。不適切な処理は、未処理の例外、情報漏洩、ユーザーエクスペリエンスの低下、セキュリティ上の脆弱性につながります。ここで紹介するパターンは、すべてのエラーを捕捉し、ログに記録したうえで適切に処理する、階層的な例外処理のアプローチを提供します。
例外処理のレイヤー
| レイヤー | 目的 | 適用範囲 |
|---|---|---|
| グローバルミドルウェア | 捕捉されなかった例外をすべて処理する | アプリケーション全体 |
| 例外フィルター | コントローラーまたはページ固有の例外を処理する | PageModel |
| Try-Catch ブロック | 特定の操作を処理する | メソッドレベル |
| エラーページ | ユーザーにわかりやすいエラーを表示する | UI |
パターン 1:グローバル例外ハンドラーの構成
Program.cs の設定
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Configure exception handling middleware (order matters!)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage(); // Detailed errors for dev
}
else
{
app.UseExceptionHandler("/Error"); // Production error page
app.UseStatusCodePagesWithReExecute("/NotFound", "?statusCode={0}");
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapRazorPages();
エラーページのモデル
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel(ILogger<ErrorModel> logger, IWebHostEnvironment env) : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public string? ErrorMessage { get; set; }
public string? StackTrace { get; set; }
public int StatusCode { get; set; } = 500;
public void OnGet(int? statusCode = null)
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
StatusCode = statusCode ?? 500;
var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error != null)
{
var ex = exceptionHandlerPathFeature.Error;
var path = exceptionHandlerPathFeature.Path;
logger.LogError(ex,
"Unhandled exception at {Path}. RequestId: {RequestId}",
path, RequestId);
// Only expose details in development
if (env.IsDevelopment())
{
ErrorMessage = ex.Message;
StackTrace = ex.StackTrace;
}
else
{
ErrorMessage = "An unexpected error occurred. Please try again later.";
}
}
}
}
Error.cshtml
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
<p class="text-muted">
Please include this ID when contacting support.
</p>
}
@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
<h3>Error Details</h3>
<p>@Model.ErrorMessage</p>
@if (!string.IsNullOrEmpty(Model.StackTrace))
{
<pre class="alert alert-secondary">@Model.StackTrace</pre>
}
}
パターン 2:ステータスコードページ
NotFound ページのモデル
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class NotFoundModel : PageModel
{
public int StatusCode { get; set; }
public string? OriginalPath { get; set; }
public void OnGet(int statusCode)
{
StatusCode = statusCode;
OriginalPath = HttpContext.Features.Get<IStatusCodeReExecuteFeature>()?.OriginalPath;
}
}
NotFound.cshtml
@page
@model NotFoundModel
@{
ViewData["Title"] = "Not Found";
}
<div class="text-center">
<h1 class="display-1">@Model.StatusCode</h1>
<h2>Page Not Found</h2>
@if (!string.IsNullOrEmpty(Model.OriginalPath))
{
<p>The page <code>@Model.OriginalPath</code> could not be found.</p>
}
<a asp-page="/Index" class="btn btn-primary">Return to Home</a>
</div>
パターン 3:カスタム例外ミドルウェア
組み込みの例外ハンドラーより細かく制御するには、カスタムミドルウェアを作成します。
public class GlobalExceptionHandlingMiddleware(RequestDelegate next, ILogger<GlobalExceptionHandlingMiddleware> logger)
{
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
logger.LogError(exception, "Unhandled exception occurred");
context.Response.ContentType = "application/json";
var response = exception switch
{
ValidationException ex => CreateValidationErrorResponse(context, ex),
NotFoundException ex => CreateNotFoundResponse(context, ex),
UnauthorizedAccessException ex => CreateUnauthorizedResponse(context, ex),
ConflictException ex => CreateConflictResponse(context, ex),
_ => CreateGenericErrorResponse(context, exception)
};
context.Response.StatusCode = response.Status ?? 500;
await context.Response.WriteAsJsonAsync(response);
}
private static ProblemDetails CreateValidationErrorResponse(HttpContext context, ValidationException ex)
{
return new ValidationProblemDetails(ex.Errors)
{
Status = StatusCodes.Status400BadRequest,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = "Validation Failed",
Detail = "One or more validation errors occurred",
Instance = context.Request.Path
};
}
private static ProblemDetails CreateNotFoundResponse(HttpContext context, NotFoundException ex)
{
return new ProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "Not Found",
Detail = ex.Message,
Instance = context.Request.Path
};
}
private static ProblemDetails CreateUnauthorizedResponse(HttpContext context, UnauthorizedAccessException ex)
{
return new ProblemDetails
{
Status = StatusCodes.Status403Forbidden,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
Title = "Forbidden",
Detail = "You do not have permission to perform this action",
Instance = context.Request.Path
};
}
private static ProblemDetails CreateConflictResponse(HttpContext context, ConflictException ex)
{
return new ProblemDetails
{
Status = StatusCodes.Status409Conflict,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
Title = "Conflict",
Detail = ex.Message,
Instance = context.Request.Path
};
}
private static ProblemDetails CreateGenericErrorResponse(HttpContext context, Exception ex)
{
return new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
Title = "Internal Server Error",
Detail = "An unexpected error occurred",
Instance = context.Request.Path
};
}
}
// Extension method
public static class ExceptionHandlingExtensions
{
public static IApplicationBuilder UseGlobalExceptionHandling(this IApplicationBuilder app)
{
return app.UseMiddleware<GlobalExceptionHandlingMiddleware>();
}
}
パターン 4:カスタム例外
さまざまなエラー状況に対応する、ドメイン固有の例外を定義します。
// Base exception
public abstract class DomainException : Exception
{
protected DomainException(string message) : base(message) { }
protected DomainException(string message, Exception inner) : base(message, inner) { }
}
// Not Found
public class NotFoundException : DomainException
{
public NotFoundException(string entityType, object id)
: base($"{entityType} with id '{id}' was not found.") { }
public NotFoundException(string message) : base(message) { }
}
// Validation
public class ValidationException : DomainException
{
public IReadOnlyDictionary<string, string[]> Errors { get; }
public ValidationException(IDictionary<string, string[]> errors)
: base("Validation failed")
{
Errors = new ReadOnlyDictionary<string, string[]>(errors);
}
public ValidationException(string propertyName, string errorMessage)
: base("Validation failed")
{
Errors = new ReadOnlyDictionary<string, string[]>(
new Dictionary<string, string[]> { [propertyName] = new[] { errorMessage } });
}
}
// Conflict
public class ConflictException : DomainException
{
public ConflictException(string message) : base(message) { }
}
// Business Rule Violation
public class BusinessRuleException : DomainException
{
public string RuleCode { get; }
public BusinessRuleException(string ruleCode, string message)
: base(message)
{
RuleCode = ruleCode;
}
}
パターン 5:ProblemDetails API
ASP.NET Core 7 以降には、ProblemDetails のサポートが組み込まれています。
// Program.cs
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Instance = ctx.HttpContext.Request.Path;
ctx.ProblemDetails.Extensions["traceId"] = Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier;
ctx.ProblemDetails.Extensions["timestamp"] = DateTimeOffset.UtcNow;
if (ctx.HttpContext.User.Identity?.IsAuthenticated == true)
{
ctx.ProblemDetails.Extensions["userId"] = ctx.HttpContext.User.Identity.Name;
}
};
});
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
カスタム ProblemDetails ファクトリー
public class CustomProblemDetailsFactory : ProblemDetailsFactory
{
public override ProblemDetails CreateProblemDetails(
HttpContext httpContext,
int? statusCode = null,
string? title = null,
string? type = null,
string? detail = null,
string? instance = null)
{
var problemDetails = new ProblemDetails
{
Status = statusCode ?? 500,
Title = title,
Type = type,
Detail = detail,
Instance = instance ?? httpContext.Request.Path
};
// Add correlation ID
if (httpContext.Request.Headers.TryGetValue("X-Correlation-Id", out var correlationId))
{
problemDetails.Extensions["correlationId"] = correlationId.ToString();
}
return problemDetails;
}
public override ValidationProblemDetails CreateValidationProblemDetails(
HttpContext httpContext,
ModelStateDictionary modelStateDictionary,
int? statusCode = null,
string? title = null,
string? type = null,
string? detail = null,
string? instance = null)
{
var validationProblemDetails = new ValidationProblemDetails(modelStateDictionary)
{
Status = statusCode ?? 400,
Title = title ?? "Validation Failed",
Type = type,
Detail = detail,
Instance = instance ?? httpContext.Request.Path
};
return validationProblemDetails;
}
}
// Register
builder.Services.AddSingleton<ProblemDetailsFactory, CustomProblemDetailsFactory>();
パターン 6:ハンドラーレベルの例外処理
public abstract class SafePageModel : PageModel
{
protected async Task<IActionResult> TryAsync(Func<Task<IActionResult>> action)
{
try
{
return await action();
}
catch (NotFoundException ex)
{
TempData["ErrorMessage"] = ex.Message;
return NotFound();
}
catch (ValidationException ex)
{
foreach (var error in ex.Errors)
{
ModelState.AddModelError(error.Key, string.Join(", ", error.Value));
}
return Page();
}
catch (ConflictException ex)
{
ModelState.AddModelError(string.Empty, ex.Message);
return Page();
}
catch (Exception ex)
{
// Log and return generic error
TempData["ErrorMessage"] = "An unexpected error occurred. Please try again.";
return RedirectToPage("/Error");
}
}
}
// Usage
public class OrderDetailsModel(IOrderService orderService) : SafePageModel
{
public Order? Order { get; set; }
public Task<IActionResult> OnGetAsync(Guid id)
{
return TryAsync(async () =>
{
Order = await orderService.GetAsync(id);
return Page();
});
}
}
パターン 7:バックグラウンドサービスの例外処理
public class EmailOutboxProcessor(ILogger<EmailOutboxProcessor> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessOutboxAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Expected during shutdown
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing email outbox");
// Wait before retrying
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
private async Task ProcessOutboxAsync(CancellationToken ct)
{
// Processing logic here
}
}
パターン 8:MediatR の例外処理ビヘイビア
public class ExceptionHandlingBehavior<TRequest, TResponse>(ILogger<ExceptionHandlingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
try
{
return await next(cancellationToken);
}
catch (DomainException)
{
// Domain exceptions are expected - rethrow for handling upstream
throw;
}
catch (Exception ex)
{
logger.LogError(ex,
"Unexpected error handling {RequestName}",
typeof(TRequest).Name);
throw new ApplicationException(
$"Error processing request {typeof(TRequest).Name}", ex);
}
}
}
アンチパターン
例外を握りつぶす
// ❌ BAD: Silent failure
try
{
await _service.DoWorkAsync();
}
catch (Exception)
{
// Nothing logged, nothing thrown!
}
// ✅ GOOD: Log and either handle or rethrow
catch (Exception ex)
{
_logger.LogError(ex, "Failed to complete work");
throw; // Or handle gracefully
}
汎用的な Exception を早い段階で捕捉する
// ❌ BAD: Catching generic exception too early prevents proper handling
try
{
var user = await _userService.GetAsync(id);
var order = await _orderService.CreateAsync(user, request);
}
catch (Exception ex) // Catches everything
{
// Can't distinguish between user not found and order creation failure
}
// ✅ GOOD: Catch specific exceptions where they occur
try
{
var user = await _userService.GetAsync(id);
}
catch (NotFoundException ex)
{
return NotFound(ex.Message);
}
try
{
var order = await _orderService.CreateAsync(user, request);
}
catch (ValidationException ex)
{
return BadRequest(ex.Errors);
}
機密情報の漏洩
// ❌ BAD: Exposing internal details in production
catch (Exception ex)
{
return Content($"Database connection failed: {ex.StackTrace}");
}
// ✅ GOOD: Generic message in production, details in development
catch (Exception ex)
{
_logger.LogError(ex, "Internal error");
if (_env.IsDevelopment())
{
return Content(ex.ToString());
}
return Content("An error occurred");
}
参考資料
コメント
Markdown 対応