// API.Core/Filters/GlobalExceptionFilter.cs using Domain.Exceptions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Data.SqlClient; using Shared.Contracts; using ValidationException = FluentValidation.ValidationException; namespace API.Core; /// /// MVC exception filter that converts unhandled exceptions raised by controller actions into /// consistent JSON error responses with appropriate HTTP status codes. /// /// Logger used to record unhandled exceptions before they are translated into a response. public class GlobalExceptionFilter(ILogger logger) : IExceptionFilter { /// /// Logs the exception and sets to an appropriate error response, /// marking the exception as handled. /// /// /// Maps exception types to responses as follows: /// /// /// /// 400 Bad Request with per-property /// validation error messages. /// /// /// /// /// 409 Conflict with a /// message. /// /// /// /// /// 404 Not Found with a /// message. /// /// /// /// /// 401 Unauthorized with a /// message. /// /// /// /// /// 403 Forbidden with a /// message. /// /// /// /// /// 503 Service Unavailable with a generic database error /// message. /// /// /// /// /// 400 Bad Request with a /// message. /// /// /// /// Any other exception → 500 Internal Server Error with a generic error message. /// /// /// /// /// The context for the unhandled exception, including the exception itself and the result to /// populate. /// public void OnException(ExceptionContext context) { logger.LogError(context.Exception, "Unhandled exception occurred"); switch (context.Exception) { case ValidationException fluentValidationException: Dictionary errors = fluentValidationException .Errors.GroupBy(e => e.PropertyName) .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()); context.Result = new BadRequestObjectResult( new { message = "Validation failed", errors } ); context.ExceptionHandled = true; break; case ConflictException ex: context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { StatusCode = 409, }; context.ExceptionHandled = true; break; case NotFoundException ex: context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { StatusCode = 404, }; context.ExceptionHandled = true; break; case UnauthorizedException ex: context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { StatusCode = 401, }; context.ExceptionHandled = true; break; case ForbiddenException ex: context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { StatusCode = 403, }; context.ExceptionHandled = true; break; case SqlException ex: context.Result = new ObjectResult( new ResponseBody { Message = "A database error occurred." } ) { StatusCode = 503, }; context.ExceptionHandled = true; break; case Domain.Exceptions.ValidationException ex: context.Result = new ObjectResult(new ResponseBody { Message = ex.Message }) { StatusCode = 400, }; context.ExceptionHandled = true; break; default: context.Result = new ObjectResult( new ResponseBody { Message = "An unexpected error occurred" } ) { StatusCode = 500, }; context.ExceptionHandled = true; break; } } }