การสร้าง Global Exception Handling Middleware ใน ASP.NET Core
Middleware เป็นจุดกลางสำหรับจัดการ HTTP Request Pipeline การสร้าง Custom Exception Middleware ช่วยจัดการข้อผิดพลาดแบบรวมศูนย์ (Centralized) ให้สม่ำเสมอทั่วแอปพลิเคชัน
โครงสร้าง Exception Middleware
using System;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext); // เรียก middleware ถัดไป
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
await HandleExceptionAsync(httpContext, ex);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
var (statusCode, message) = exception switch
{
ArgumentNullException => (HttpStatusCode.BadRequest, "Required parameter is missing"),
ArgumentException => (HttpStatusCode.BadRequest, exception.Message),
KeyNotFoundException => (HttpStatusCode.NotFound, "Resource not found"),
UnauthorizedAccessException => (HttpStatusCode.Unauthorized, "Unauthorized access"),
_ => (HttpStatusCode.InternalServerError, "An internal server error occurred")
};
context.Response.StatusCode = (int)statusCode;
var response = new ErrorResponse
{
StatusCode = statusCode,
Message = message,
// ระบุ Path เพื่อช่วย debug
Path = context.Request.Path
};
var json = JsonSerializer.Serialize(response, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
await context.Response.WriteAsync(json);
}
}
public class ErrorResponse
{
public HttpStatusCode StatusCode { get; set; }
public string Message { get; set; }
public string Path { get; set; }
public string TraceId { get; set; } = System.Diagnostics.Activity.Current?.Id;
}
Extension Method เพื่อง่ายต่อการใช้งาน
using Microsoft.AspNetCore.Builder;
public static class ExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ExceptionMiddleware>();
}
}
ลงทะเบียนใน Program.cs (.NET 6+) หรือ Startup.cs
// Program.cs (.NET 6+)
var builder = WebApplication.CreateBuilder(args);
// เพิ่ม logging
builder.Services.AddLogging();
var app = builder.Build();
// ใช้ Exception Middleware (ควรอยู่บนสุดของ pipeline)
app.UseCustomExceptionHandler();
// Middleware อื่นๆ
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
Built-in Exception Handling (ทางเลือกง่ายกว่า)
ASP.NET Core มี middleware ตัวจัดการ error built-in แล้ว:
1. Developer Exception Page (Development only)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage(); // แสดง stack trace เต็ม
}
2. Exception Handler (Production)
// นำทางไปยัง error handling endpoint
app.UseExceptionHandler("/error");
// สร้าง Error Controller
[ApiController]
[Route("error")]
public class ErrorController : ControllerBase
{
[HttpGet]
public IActionResult Error()
{
var exception = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error;
var logger = HttpContext.RequestServices.GetService<ILogger<ErrorController>>();
logger?.LogError(exception, "Unhandled exception");
return Problem(
title: "An error occurred",
statusCode: 500,
detail: exception?.Message
);
}
}
3. Status Code Pages
// แปลง status code 404, 500 เป็น response ที่อ่านง่าย
app.UseStatusCodePages(async context =>
{
context.HttpContext.Response.ContentType = "application/json";
await context.HttpContext.Response.WriteAsync(
JsonSerializer.Serialize(new {
Error = context.HttpContext.Response.StatusCode == 404 ? "Not Found" : "Error",
StatusCode = context.HttpContext.Response.StatusCode
})
);
});
// หรือใช้กับ Razor Pages
app.UseStatusCodePagesWithReExecute("/Error/{0}");
แนวทางปฏิบัติที่ดี (Best Practices)
| หลักการ | คำอธิบาย |
|---|---|
| Log Exception | Log ทุก exception พร้อม context (TraceId, User, Path) |
| Don't Leak Details | Production: ไม่คืน stack trace/internal detail ให้ client |
| Consistent Format | ใช้ response format เดียวกันทั่วแอป (RFC 7807 ProblemDetails) |
| Handle Specific Exceptions | Map exception ประเภทต่างๆ ไป status code ที่เหมาะสม |
| Order Matters | วาง Exception Middleware บนสุด (UseExceptionHandler อยู่บนสุด) |
ProblemDetails (RFC 7807) - มาตรฐานสำหรับ API Error
// ใช้ built-in ProblemDetails (ASP.NET Core 3.1+)
app.UseExceptionHandler("/error");
[ApiController]
public class ErrorController : ControllerBase
{
[Route("error")]
public IActionResult Error()
{
var feature = HttpContext.Features.Get<IExceptionHandlerFeature>();
var problem = new ProblemDetails
{
Title = "An error occurred",
Status = 500,
Detail = feature?.Error.Message,
Instance = HttpContext.Request.Path,
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
};
// เพิ่มข้อมูล custom
problem.Extensions["traceId"] = System.Diagnostics.Activity.Current?.Id;
return new ObjectResult(problem) { StatusCode = 500 };
}
}
สรุป
- Custom Middleware - ควบคุมได้เต็มที่ เหมาะกับ complex logic, logging แบบกำหนดเอง
- UseExceptionHandler - เรียบง่าย พอสำหรับส่วนใหญ่ ใช้กับ Error Controller
- UseDeveloperExceptionPage - เฉพาะ Development เท่านั้น
- ProblemDetails - มาตรฐานสากล สำหรับ REST API