feat(ExceptionHandlingMiddleware): Add to handle exceptions by middleware

This commit is contained in:
tekh 2025-07-30 17:16:10 +02:00
parent 78f2788388
commit 63df235943
8 changed files with 175 additions and 112 deletions

View File

@ -1,76 +1,74 @@
using Microsoft.AspNetCore.Mvc; using System.Security.Claims;
using System.Security.Claims;
using WorkFlow.API.Attributes; using WorkFlow.API.Attributes;
namespace WorkFlow.API.Controllers namespace WorkFlow.API.Controllers;
[APIKeyAuth]
public static class ControllerExtensions
{ {
[APIKeyAuth] public static bool TryGetUserId(this ClaimsPrincipal user, out int id) => int.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier), out id);
public static class ControllerExtensions
public static bool TryGetUsername(this ClaimsPrincipal user, out string username)
{ {
public static bool TryGetUserId(this ClaimsPrincipal user, out int id) => int.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier), out id); var value = user.FindFirstValue(ClaimTypes.Name);
public static bool TryGetUsername(this ClaimsPrincipal user, out string username) if (value is null)
{ {
var value = user.FindFirstValue(ClaimTypes.Name); username = string.Empty;
return false;
if (value is null)
{
username = string.Empty;
return false;
}
else
{
username = value;
return true;
}
} }
else
public static bool TryGetName(this ClaimsPrincipal user, out string name)
{ {
var value = user.FindFirstValue(ClaimTypes.Surname); username = value;
return true;
if (value is null)
{
name = string.Empty;
return false;
}
else
{
name = value;
return true;
}
} }
}
public static bool TryGetPrename(this ClaimsPrincipal user, out string prename) public static bool TryGetName(this ClaimsPrincipal user, out string name)
{
var value = user.FindFirstValue(ClaimTypes.Surname);
if (value is null)
{ {
var value = user.FindFirstValue(ClaimTypes.GivenName); name = string.Empty;
return false;
if (value is null)
{
prename = string.Empty;
return false;
}
else
{
prename = value;
return true;
}
} }
else
public static bool TryGetEmail(this ClaimsPrincipal user, out string email)
{ {
var value = user.FindFirstValue(ClaimTypes.Email); name = value;
return true;
}
}
if (value is null) public static bool TryGetPrename(this ClaimsPrincipal user, out string prename)
{ {
email = string.Empty; var value = user.FindFirstValue(ClaimTypes.GivenName);
return false;
} if (value is null)
else {
{ prename = string.Empty;
email = value; return false;
return true; }
} else
{
prename = value;
return true;
}
}
public static bool TryGetEmail(this ClaimsPrincipal user, out string email)
{
var value = user.FindFirstValue(ClaimTypes.Email);
if (value is null)
{
email = string.Empty;
return false;
}
else
{
email = value;
return true;
} }
} }
} }

View File

@ -13,7 +13,7 @@ namespace WorkFlow.API.Controllers;
public class PlaceholderAuthController : ControllerBase public class PlaceholderAuthController : ControllerBase
{ {
[HttpPost] [HttpPost]
public IActionResult CreateTokenViaBody([FromBody] Login login) public IActionResult CreateToken([FromBody] Login login)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -25,21 +25,13 @@ public class ProfileController : ControllerBase
[HttpGet] [HttpGet]
public async Task<IActionResult> GetAsync() public async Task<IActionResult> GetAsync()
{ {
try if (!User.TryGetUserId(out var userId))
{ {
if (!User.TryGetUserId(out var userId)) _logger.LogError("Invalid user ID: Retrieved ID is null or not an integer.");
{ return Unauthorized("Failed to retrieve user identity.");
_logger.LogError("Invalid user ID: Retrieved ID is null or not an integer."); }
return Unauthorized("Failed to retrieve user identity.");
}
var profile = await _mediator.ReadProfileAsync(userId); var profile = await _mediator.ReadProfileAsync(userId);
return profile is null ? NotFound() : Ok(profile); return profile is null ? NotFound() : Ok(profile);
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(500);
}
} }
} }

View File

@ -25,26 +25,18 @@ public class UserController : ControllerBase
[HttpGet] [HttpGet]
public async Task<IActionResult> GetAsync() public async Task<IActionResult> GetAsync()
{ {
try if (!User.TryGetUserId(out var id))
{ {
if (!User.TryGetUserId(out var id)) logger.LogError("Authorization failed: User ID claim not found.");
{ return Unauthorized("Failed to retrieve user identity.");
logger.LogError("Authorization failed: User ID claim not found."); }
return Unauthorized("Failed to retrieve user identity.");
}
return await userService.ReadByIdAsync(id).ThenAsync( return await userService.ReadByIdAsync(id).ThenAsync(
Success: Ok, Success: Ok,
Fail: IActionResult (msg, ntc) => Fail: IActionResult (msg, ntc) =>
{ {
logger.LogNotice(ntc); logger.LogNotice(ntc);
return NotFound(); return NotFound();
}); });
}
catch (Exception ex)
{
logger.LogError(ex, "An unexpected error occurred while processing the request: {Message}", ex.Message);
return StatusCode(StatusCodes.Status500InternalServerError, "An internal server error occurred.");
}
} }
} }

View File

@ -0,0 +1,84 @@
using DigitalData.Core.Exceptions;
using System.Net;
using System.Text.Json;
namespace WorkFlow.API.Middleware;
//TODO: Fix and use DigitalData.Core.Exceptions.Middleware
/// <summary>
/// Middleware for handling exceptions globally in the application.
/// Captures exceptions thrown during the request pipeline execution,
/// logs them, and returns an appropriate HTTP response with a JSON error message.
/// </summary>
[Obsolete("Use DigitalData.Core.Exceptions.Middleware")]
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
/// </summary>
/// <param name="next">The next middleware in the request pipeline.</param>
/// <param name="logger">The logger instance for logging exceptions.</param>
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Invokes the middleware to handle the HTTP request.
/// </summary>
/// <param name="context">The HTTP context of the current request.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context); // Continue down the pipeline
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex, _logger);
}
}
/// <summary>
/// Handles exceptions by logging them and writing an appropriate JSON response.
/// </summary>
/// <param name="context">The HTTP context of the current request.</param>
/// <param name="exception">The exception that occurred.</param>
/// <param name="logger">The logger instance for logging the exception.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger)
{
context.Response.ContentType = "application/json";
string message;
switch (exception)
{
case BadRequestException badRequestEx:
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
message = badRequestEx.Message;
break;
case NotFoundException notFoundEx:
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
message = notFoundEx.Message;
break;
default:
logger.LogError(exception, "Unhandled exception occurred.");
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
message = "An unexpected error occurred.";
break;
}
await context.Response.WriteAsync(JsonSerializer.Serialize(new
{
message
}));
}
}

View File

@ -1,22 +1,22 @@
using WorkFlow.Application; using DigitalData.Auth.Client;
using Microsoft.EntityFrameworkCore; using DigitalData.Core.Abstractions.Security.Extensions;
using WorkFlow.Infrastructure;
using DigitalData.Core.Application; using DigitalData.Core.Application;
using DigitalData.UserManager.Application.DTOs.User; using DigitalData.UserManager.Application.DTOs.User;
using DigitalData.UserManager.DependencyInjection;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using WorkFlow.API.Models; using Microsoft.OpenApi.Models;
using NLog; using NLog;
using NLog.Web; using NLog.Web;
using WorkFlow.API;
using WorkFlow.API.Extensions; using WorkFlow.API.Extensions;
using WorkFlow.API.Filters; using WorkFlow.API.Filters;
using Microsoft.OpenApi.Models; using WorkFlow.API.Middleware;
using DigitalData.Auth.Client; using WorkFlow.API.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer; using WorkFlow.Application;
using WorkFlow.API; using WorkFlow.Infrastructure;
using Microsoft.Extensions.Options;
using DigitalData.Core.Abstractions.Security.Extensions;
using DigitalData.UserManager.DependencyInjection;
using DigitalData.Core.Exceptions.Middleware;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized."); logger.Info("Logging initialized.");
@ -57,8 +57,6 @@ try
Claims = user.ToClaimList().ToDictionary(claim => claim.Type, claim => claim.Value as object) Claims = user.ToClaimList().ToDictionary(claim => claim.Type, claim => claim.Value as object)
}); });
builder.Services.ConfigureGlobalExceptionHandler();
bool disableAPIKeyAuth = config.GetValue<bool>("DisableAPIKeyAuth"); bool disableAPIKeyAuth = config.GetValue<bool>("DisableAPIKeyAuth");
if (disableAPIKeyAuth) if (disableAPIKeyAuth)
builder.Services.AddAPIKeyAuth(new APIKeyAuthOptions()); builder.Services.AddAPIKeyAuth(new APIKeyAuthOptions());
@ -153,6 +151,8 @@ try
lazyProvider.Factory = () => app.Services; lazyProvider.Factory = () => app.Services;
app.UseMiddleware<ExceptionHandlingMiddleware>();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (app.Configuration.GetValue<bool>("EnableSwagger")) if (app.Configuration.GetValue<bool>("EnableSwagger"))
{ {
@ -168,8 +168,6 @@ try
app.MapControllers(); app.MapControllers();
app.UseGlobalExceptionHandler();
app.Run(); app.Run();
} }
catch (Exception ex) catch (Exception ex)

View File

@ -22,7 +22,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" /> <PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.0.0" /> <PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.0.0" />
<PackageReference Include="DigitalData.Core.Exceptions.Middleware" Version="1.0.0" /> <PackageReference Include="DigitalData.Core.Exceptions" Version="1.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" />
<PackageReference Include="NLog" Version="5.3.4" /> <PackageReference Include="NLog" Version="5.3.4" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.14" /> <PackageReference Include="NLog.Web.AspNetCore" Version="5.3.14" />

View File

@ -4,7 +4,6 @@ namespace WorkFlow.Application;
public static class DependencyInjection public static class DependencyInjection
{ {
[Obsolete("Use MediatR")]
public static IServiceCollection AddWorkFlowServices(this IServiceCollection services, Action<WorkFlowServiceOptions>? options = null) public static IServiceCollection AddWorkFlowServices(this IServiceCollection services, Action<WorkFlowServiceOptions>? options = null)
{ {
WorkFlowServiceOptions diOptions = new(); WorkFlowServiceOptions diOptions = new();