// 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;
}
}
}