Custom Exception Hierarchy
public abstract class AppException : Exception {
public abstract int StatusCode { get; }
public abstract string Code { get; }
protected AppException(string msg) : base(msg) {}
}
public class NotFoundException : AppException {
public override int StatusCode => 404;
public override string Code => "NOT_FOUND";
public NotFoundException(string name, object id) :
base($"{name} with id {id} not found") {}
}
public class ValidationException : AppException {
public override int StatusCode => 422;
public override string Code => "VALIDATION_FAILED";
public Dictionary<string, string[]> Errors { get; }
public ValidationException(Dictionary<string, string[]> errors) :
base("Validation failed") => Errors = errors;
}
public class ConflictException : AppException {
public override int StatusCode => 409;
public override string Code => "CONFLICT";
public ConflictException(string msg) : base(msg) {}
}
public class UnauthorizedException : AppException {
public override int StatusCode => 401;
public override string Code => "UNAUTHORIZED";
public UnauthorizedException(string msg = "Unauthorized") : base(msg) {}
}
Global Exception Middleware
public class ExceptionMiddleware {
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
private readonly IHostEnvironment _env;
public ExceptionMiddleware(RequestDelegate next,
ILogger<ExceptionMiddleware> logger, IHostEnvironment env) {
_next = next; _logger = logger; _env = env;
}
public async Task InvokeAsync(HttpContext ctx) {
try { await _next(ctx); }
catch (Exception ex) { await HandleAsync(ctx, ex); }
}
private async Task HandleAsync(HttpContext ctx, Exception ex) {
_logger.LogError(ex, "Unhandled: {Path}", ctx.Request.Path);
var (status, code, message) = ex switch {
AppException ae => (ae.StatusCode, ae.Code, ae.Message),
ArgumentException => (400, "BAD_REQUEST", ex.Message),
UnauthorizedAccessException => (401, "UNAUTHORIZED", "Access denied"),
KeyNotFoundException => (404, "NOT_FOUND", ex.Message),
_ => (500, "INTERNAL_ERROR", _env.IsDevelopment() ? ex.Message : "Server error")
};
ctx.Response.StatusCode = status;
ctx.Response.ContentType = "application/problem+json";
var problem = new {
type = $"https://httpstatuses.com/{status}",
title = code,
status,
detail = message,
traceId = ctx.TraceIdentifier,
errors = (ex is ValidationException ve) ? ve.Errors : null,
stackTrace = _env.IsDevelopment() ? ex.StackTrace : null
};
await ctx.Response.WriteAsJsonAsync(problem);
}
}
Register
// Program.cs — সবার আগে
app.UseMiddleware<ExceptionMiddleware>();
.NET 8+ — IExceptionHandler ⭐
public class GlobalExceptionHandler : IExceptionHandler {
public async ValueTask<bool> TryHandleAsync(
HttpContext ctx, Exception ex, CancellationToken ct)
{
var (status, msg) = ex switch {
NotFoundException => (404, ex.Message),
ValidationException => (422, ex.Message),
_ => (500, "Server error")
};
ctx.Response.StatusCode = status;
await ctx.Response.WriteAsJsonAsync(new { error = msg }, ct);
return true;
}
}
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
ProblemDetails (RFC 7807)
{
"type": "https://example.com/probs/out-of-credit",
"title": "Out of credit",
"status": 403,
"detail": "Your balance is 30",
"instance": "/account/12345",
"traceId": "00-abc-123",
"errors": { ... }
}
Use in Controller
[HttpGet("{id}")]
public async Task<ActionResult<Product>> Get(int id) {
var p = await _db.Products.FindAsync(id);
if (p == null) throw new NotFoundException("Product", id);
return p;
}