From db61171ada18771107a8716cb8dd1bfa9789d13f Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:30:58 +0300 Subject: [PATCH] Add Global Exception Handling --- .../Exceptions/BaseApplicationException.cs | 7 ++ .../Exceptions/NotFoundException.cs | 7 ++ .../Users/DeleteUserRoleHandler.cs | 4 +- .../Users/GetUserRolesHandler.cs | 4 +- .../Users/SetUserRoleHandler.cs | 4 +- .../Controllers/Users/UserRoleController.cs | 6 +- .../Middleware/ExceptionHandlingMiddleware.cs | 75 +++++++++++++++++++ src/Imprink.WebApi/Startup.cs | 2 +- 8 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 src/Imprink.Application/Exceptions/BaseApplicationException.cs create mode 100644 src/Imprink.Application/Exceptions/NotFoundException.cs create mode 100644 src/Imprink.WebApi/Middleware/ExceptionHandlingMiddleware.cs diff --git a/src/Imprink.Application/Exceptions/BaseApplicationException.cs b/src/Imprink.Application/Exceptions/BaseApplicationException.cs new file mode 100644 index 0000000..7ce0b6b --- /dev/null +++ b/src/Imprink.Application/Exceptions/BaseApplicationException.cs @@ -0,0 +1,7 @@ +namespace Imprink.Application.Exceptions; + +public abstract class BaseApplicationException : Exception +{ + protected BaseApplicationException(string message) : base(message) { } + protected BaseApplicationException(string message, Exception innerException) : base(message, innerException) { } +} \ No newline at end of file diff --git a/src/Imprink.Application/Exceptions/NotFoundException.cs b/src/Imprink.Application/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..ade782f --- /dev/null +++ b/src/Imprink.Application/Exceptions/NotFoundException.cs @@ -0,0 +1,7 @@ +namespace Imprink.Application.Exceptions; + +public class NotFoundException : BaseApplicationException +{ + public NotFoundException(string message) : base(message) { } + public NotFoundException(string message, Exception innerException) : base(message, innerException) { } +} \ No newline at end of file diff --git a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs index ca536f8..ec950fe 100644 --- a/src/Imprink.Application/Users/DeleteUserRoleHandler.cs +++ b/src/Imprink.Application/Users/DeleteUserRoleHandler.cs @@ -1,3 +1,4 @@ +using Imprink.Application.Exceptions; using Imprink.Application.Users.Dtos; using Imprink.Domain.Entities.Users; using MediatR; @@ -10,7 +11,8 @@ public class DeleteUserRoleHandler(IUnitOfWork uw) : IRequestHandler Handle(DeleteUserRoleCommand request, CancellationToken cancellationToken) { - if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) return null; + if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) + throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); var userRole = new UserRole { diff --git a/src/Imprink.Application/Users/GetUserRolesHandler.cs b/src/Imprink.Application/Users/GetUserRolesHandler.cs index ef2091a..dca1ed0 100644 --- a/src/Imprink.Application/Users/GetUserRolesHandler.cs +++ b/src/Imprink.Application/Users/GetUserRolesHandler.cs @@ -1,3 +1,4 @@ +using Imprink.Application.Exceptions; using Imprink.Application.Users.Dtos; using MediatR; @@ -9,7 +10,8 @@ public class GetUserRolesHandler(IUnitOfWork uw): IRequestHandler> Handle(GetUserRolesCommand request, CancellationToken cancellationToken) { - if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) return []; + if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) + throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); var roles = await uw.UserRoleRepository.GetUserRolesAsync(request.Sub, cancellationToken); diff --git a/src/Imprink.Application/Users/SetUserRoleHandler.cs b/src/Imprink.Application/Users/SetUserRoleHandler.cs index 3fe8ca7..76f5349 100644 --- a/src/Imprink.Application/Users/SetUserRoleHandler.cs +++ b/src/Imprink.Application/Users/SetUserRoleHandler.cs @@ -1,3 +1,4 @@ +using Imprink.Application.Exceptions; using Imprink.Application.Users.Dtos; using Imprink.Domain.Entities.Users; using MediatR; @@ -10,7 +11,8 @@ public class SetUserRoleHandler(IUnitOfWork uw) : IRequestHandler Handle(SetUserRoleCommand request, CancellationToken cancellationToken) { - if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) return null; + if (!await uw.UserRepository.UserExistsAsync(request.Sub, cancellationToken)) + throw new NotFoundException("User with ID: " + request.Sub + " does not exist."); var userRole = new UserRole { diff --git a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs b/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs index 62da421..34a2926 100644 --- a/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs +++ b/src/Imprink.WebApi/Controllers/Users/UserRoleController.cs @@ -11,7 +11,7 @@ namespace Imprink.WebApi.Controllers.Users; public class UserRoleController(IMediator mediator) : ControllerBase { [Authorize] - [HttpGet("/me")] + [HttpGet("me")] public async Task GetMyRoles() { var claims = User.Claims as Claim[] ?? User.Claims.ToArray(); @@ -23,7 +23,7 @@ public class UserRoleController(IMediator mediator) : ControllerBase } [Authorize(Roles = "Admin")] - [HttpPost("/set")] + [HttpPost("set")] public async Task SetUserRole(SetUserRoleCommand command) { var userRole = await mediator.Send(command); @@ -35,7 +35,7 @@ public class UserRoleController(IMediator mediator) : ControllerBase } [Authorize(Roles = "Admin")] - [HttpPost("/unset")] + [HttpPost("unset")] public async Task UnsetUserRole(DeleteUserRoleCommand command) { var userRole = await mediator.Send(command); diff --git a/src/Imprink.WebApi/Middleware/ExceptionHandlingMiddleware.cs b/src/Imprink.WebApi/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..eb70eab --- /dev/null +++ b/src/Imprink.WebApi/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,75 @@ +using System.Net; +using System.Text.Json; +using Imprink.Application.Exceptions; + +namespace Imprink.WebApi.Middleware; + +public class ExceptionHandlingMiddleware( + RequestDelegate next, + ILogger logger) +{ + public async Task InvokeAsync(HttpContext context) + { + try + { + await next(context); + } + catch (Exception ex) + { + var (_, _, shouldLog) = GetStatusCodeAndMessage(ex); + + if (shouldLog) + { + logger.LogError(ex, "An unhandled exception occurred: {Message}", ex.Message); + } + else + { + logger.LogInformation("Handled: {Message}", ex.Message); + } + + await HandleExceptionAsync(context, ex); + } + } + + private static async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + context.Response.ContentType = "application/json"; + + var (statusCode, message, _) = GetStatusCodeAndMessage(exception); + + context.Response.StatusCode = (int)statusCode; + + var response = new + { + error = new + { + message, + timestamp = DateTime.UtcNow, + } + }; + + var jsonResponse = JsonSerializer.Serialize(response, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + await context.Response.WriteAsync(jsonResponse); + } + + private static (HttpStatusCode statusCode, string message, bool shouldLog) GetStatusCodeAndMessage(Exception exception) + { + return exception switch + { + NotFoundException => (HttpStatusCode.NotFound, exception.Message, false), + _ => (HttpStatusCode.InternalServerError, "An internal server error occurred", true) + }; + } +} + +public static class GlobalExceptionHandlingMiddlewareExtensions +{ + public static void UseGlobalExceptionHandling(this IApplicationBuilder builder) + { + builder.UseMiddleware(); + } +} diff --git a/src/Imprink.WebApi/Startup.cs b/src/Imprink.WebApi/Startup.cs index cf44d43..60e1eae 100644 --- a/src/Imprink.WebApi/Startup.cs +++ b/src/Imprink.WebApi/Startup.cs @@ -140,13 +140,13 @@ public static class Startup } } + app.UseGlobalExceptionHandling(); app.UseRequestTiming(); if (env.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); - app.UseDeveloperExceptionPage(); } else {