// API.Core/Filters/GlobalExceptionFilter.cs using API.Core.Contracts.Common; using Domain.Exceptions; using FluentValidation; using Microsoft.Data.SqlClient; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; 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 FluentValidation.ValidationException fluentValidationException: var 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; } } }