روش مدرن: IExceptionHandler ✨
ASP.NET Core 8
اینترفیس IExceptionHandler را معرفی کرد، و این یک تغییردهنده بازی است. به جای یک middleware عظیم که همه چیز را مدیریت میکند، میتوانیم handlerهای متمرکزی برای انواع خاص استثناها ایجاد کنیم.
اینطور کار میکند:
internal sealed class GlobalExceptionHandler(
IProblemDetailsService problemDetailsService,
ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
logger.LogError(exception, "Unhandled exception occurred");
httpContext.Response.StatusCode = exception switch
{
ApplicationException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};
return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
// ...
});
}
}
نکته کلیدی در اینجا مقدار بازگشتی است. اگر handler شما بتواند استثنا را مدیریت کند، true برگردانید. اگر نه، false برگردانید و اجازه دهید handler بعدی تلاش کند.
فراموش نکنید آن را با DI و در پایپلاین درخواست ثبت کنید:
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
// و در پایپلاین شما
app.UseExceptionHandler();
این رویکرد بسیار تمیزتر است. هر handler یک وظیفه دارد و کد به راحتی قابل تست و نگهداری است.
زنجیرهای کردن Exception Handlerها ⛓️
شما میتوانید چندین exception handler را با هم زنجیرهای کنید و آنها به ترتیبی که ثبت کردهاید اجرا میشوند. ASP.NET Core از اولین handler که true از TryHandleAsync برگرداند، استفاده خواهد کرد.
مثال: یکی برای خطاهای اعتبارسنجی، یکی به عنوان fallback سراسری.
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
بیایید بگوییم شما از FluentValidation استفاده میکنید (و باید هم استفاده کنید). در اینجا یک راهاندازی کامل آمده است:
internal sealed class ValidationExceptionHandler(
IProblemDetailsService problemDetailsService,
ILogger<ValidationExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if (exception is not ValidationException validationException)
{
return false;
}
logger.LogError(exception, "Unhandled exception occurred");
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
var context = new ProblemDetailsContext { /* ... */ };
var errors = validationException.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key.ToLowerInvariant(),
g => g.Select(e => e.ErrorMessage).ToArray()
);
context.ProblemDetails.Extensions.Add("errors", errors);
return await problemDetailsService.TryWriteAsync(context);
}
}
ترتیب اجرا مهم است. فریمورک هر handler را به ترتیبی که ثبت کردهاید امتحان میکند. پس handlerهای خاصتر خود را اول و handler catch-all خود را آخر قرار دهید.
خلاصه 📝
ما راه درازی را از روزهای ساخت دستی پاسخهای خطا در middleware پیمودهایم. تکامل به این شکل است:
1️⃣ Middleware:
ساده، همه جا کار میکند، اما سریع پیچیده میشود.
2️⃣ IProblemDetailsService:
فرمت پاسخ را استاندارد میکند، هنوز قابل مدیریت است.
3️⃣ IExceptionHandler:
مدرن، قابل تست، و به زیبایی مقیاسپذیر است.
نکته کلیدی؟ 🔑
اجازه ندهید مدیریت خطا یک فکر آخر باشد. آن را زود راهاندازی کنید، یکپارچهاش کنید، و کاربران شما (و خود آیندهتان) وقتی همه چیز به ناچار خراب شد، از شما تشکر خواهند کرد.
🔖 هشتگها:
#CSharp #DotNet #ErrorHandling #ExceptionHandling #SoftwareArchitecture