// API.Core/Filters/GlobalExceptionFilter.cs
using Shared.Contracts;
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;
}
}
}