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;
namespace WorkFlow.API.Controllers
namespace WorkFlow.API.Controllers;
[APIKeyAuth]
public static class ControllerExtensions
{
[APIKeyAuth]
public static class ControllerExtensions
public static bool TryGetUserId(this ClaimsPrincipal user, out int id) => int.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier), out id);
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);
if (value is null)
{
username = string.Empty;
return false;
}
else
{
username = value;
return true;
}
username = string.Empty;
return false;
}
public static bool TryGetName(this ClaimsPrincipal user, out string name)
else
{
var value = user.FindFirstValue(ClaimTypes.Surname);
if (value is null)
{
name = string.Empty;
return false;
}
else
{
name = value;
return true;
}
username = 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);
if (value is null)
{
prename = string.Empty;
return false;
}
else
{
prename = value;
return true;
}
name = string.Empty;
return false;
}
public static bool TryGetEmail(this ClaimsPrincipal user, out string email)
else
{
var value = user.FindFirstValue(ClaimTypes.Email);
name = value;
return true;
}
}
if (value is null)
{
email = string.Empty;
return false;
}
else
{
email = value;
return true;
}
public static bool TryGetPrename(this ClaimsPrincipal user, out string prename)
{
var value = user.FindFirstValue(ClaimTypes.GivenName);
if (value is null)
{
prename = string.Empty;
return false;
}
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
{
[HttpPost]
public IActionResult CreateTokenViaBody([FromBody] Login login)
public IActionResult CreateToken([FromBody] Login login)
{
throw new NotImplementedException();
}

View File

@ -25,21 +25,13 @@ public class ProfileController : ControllerBase
[HttpGet]
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);
return profile is null ? NotFound() : Ok(profile);
}
catch (Exception ex)
{
_logger.LogError(ex, "{Message}", ex.Message);
return StatusCode(500);
}
var profile = await _mediator.ReadProfileAsync(userId);
return profile is null ? NotFound() : Ok(profile);
}
}

View File

@ -25,26 +25,18 @@ public class UserController : ControllerBase
[HttpGet]
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(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
logger.LogNotice(ntc);
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.");
}
return await userService.ReadByIdAsync(id).ThenAsync(
Success: Ok,
Fail: IActionResult (msg, ntc) =>
{
logger.LogNotice(ntc);
return NotFound();
});
}
}

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

View File

@ -22,7 +22,7 @@
<ItemGroup>
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
<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="NLog" Version="5.3.4" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.14" />

View File

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