This commit is contained in:
2025-10-27 17:39:18 -04:00
commit 31f723bea4
1579 changed files with 642409 additions and 0 deletions

View File

@@ -0,0 +1,229 @@
using System.Text.Json;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Prefab.Handler;
using Prefab.Catalog.Domain.Exceptions;
using Prefab.Shared;
using Microsoft.EntityFrameworkCore;
namespace Prefab.Web.Shared;
public class ExceptionHandler(RequestDelegate next, ILogger<ExceptionHandler> logger, IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions> jsonOptions)
{
private readonly JsonSerializerOptions _jsonOptions = jsonOptions?.Value?.SerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web);
public async Task InvokeAsync(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
if (context.Response.HasStarted)
{
logger.LogWarning("Response already started for {Method} {Path}. Rethrowing exception.", context.Request.Method, context.Request.Path);
throw;
}
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var (status, problem) = MapException(context, exception);
problem.Status ??= status;
problem.Instance ??= context.Request.Path;
problem.Extensions["traceId"] = context.TraceIdentifier;
if (!problem.Extensions.ContainsKey("method"))
{
problem.Extensions["method"] = context.Request.Method;
}
if (context.RequestServices.GetService<IHandlerContextAccessor>()?.Current is { } handlerContext)
{
if (!string.IsNullOrWhiteSpace(handlerContext.CorrelationId))
{
problem.Extensions["correlationId"] = handlerContext.CorrelationId;
}
if (!string.IsNullOrWhiteSpace(handlerContext.RequestId))
{
problem.Extensions["requestId"] = handlerContext.RequestId;
}
}
else if (context.Items.TryGetValue(HandlerContextItems.HttpContextKey, out var ctxObj) && ctxObj is HandlerContext storedContext)
{
if (!string.IsNullOrWhiteSpace(storedContext.CorrelationId))
{
problem.Extensions["correlationId"] = storedContext.CorrelationId;
}
if (!string.IsNullOrWhiteSpace(storedContext.RequestId))
{
problem.Extensions["requestId"] = storedContext.RequestId;
}
}
else if (!problem.Extensions.ContainsKey("correlationId"))
{
problem.Extensions["correlationId"] = context.TraceIdentifier;
}
if (problem is ValidationProblemDetails validation)
{
if (!problem.Extensions.ContainsKey("errors"))
{
problem.Extensions["errors"] = validation.Errors;
}
if (string.IsNullOrWhiteSpace(problem.Detail))
{
var firstError = validation.Errors.Values.SelectMany(values => values)
.FirstOrDefault(message => !string.IsNullOrWhiteSpace(message));
if (!string.IsNullOrWhiteSpace(firstError))
{
problem.Detail = firstError;
}
}
}
context.Response.StatusCode = status;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsync(JsonSerializer.Serialize(problem, _jsonOptions));
}
private (int StatusCode, ProblemDetails Problem) MapException(HttpContext context, Exception exception) =>
exception switch
{
ValidationException validationException => MapValidationException(validationException),
CatalogNotFoundException notFound => MapNotFoundException(notFound),
RemoteProblemException remoteException => MapRemoteProblemException(remoteException),
DomainException domainException => MapDomainException(domainException),
DbUpdateConcurrencyException concurrencyException => MapConcurrencyException(concurrencyException),
_ => MapUnexpectedException(exception)
};
private static (int StatusCode, ProblemDetails Problem) MapValidationException(ValidationException exception)
{
var errors = exception.Errors
.GroupBy(error => string.IsNullOrWhiteSpace(error.PropertyName) ? string.Empty : error.PropertyName)
.ToDictionary(
group => group.Key,
group => group.Select(error => error.ErrorMessage ?? string.Empty).ToArray());
var details = new ValidationProblemDetails(errors)
{
Title = "Validation failed.",
Type = "https://prefab.dev/problems/validation-error",
Status = StatusCodes.Status400BadRequest
};
return (details.Status.Value, details);
}
private static (int StatusCode, ProblemDetails Problem) MapNotFoundException(CatalogNotFoundException exception)
{
var details = new ProblemDetails
{
Title = "Resource not found.",
Detail = exception.Message,
Type = "https://prefab.dev/problems/not-found",
Status = StatusCodes.Status404NotFound
};
details.Extensions["resource"] = exception.Resource;
details.Extensions["identifier"] = exception.Identifier;
return (details.Status.Value, details);
}
private (int StatusCode, ProblemDetails Problem) MapRemoteProblemException(RemoteProblemException exception)
{
var status = (int)exception.StatusCode;
ProblemDetails details;
if (!string.IsNullOrWhiteSpace(exception.ResponseBody))
{
try
{
details = JsonSerializer.Deserialize<ProblemDetails>(exception.ResponseBody, new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? new ProblemDetails();
}
catch (JsonException)
{
details = new ProblemDetails
{
Detail = exception.ResponseBody
};
}
}
else
{
details = new ProblemDetails();
}
details.Status ??= status;
details.Title ??= "Remote request failed.";
details.Detail ??= "The downstream service returned an error.";
details.Type ??= "https://prefab.dev/problems/remote-error";
return (details.Status.Value, details);
}
private static (int StatusCode, ProblemDetails Problem) MapDomainException(DomainException exception)
{
var details = new ProblemDetails
{
Title = "Request could not be processed.",
Detail = exception.Message,
Type = "https://prefab.dev/problems/domain-error",
Status = StatusCodes.Status400BadRequest
};
return (details.Status.Value, details);
}
private (int StatusCode, ProblemDetails Problem) MapConcurrencyException(DbUpdateConcurrencyException exception)
{
logger.LogWarning(exception, "Concurrency conflict encountered.");
var details = new ProblemDetails
{
Title = "Concurrency conflict.",
Detail = "The resource was modified by another process. Refresh and retry your request.",
Type = "https://prefab.dev/problems/concurrency-conflict",
Status = StatusCodes.Status409Conflict
};
return (details.Status.Value, details);
}
private (int StatusCode, ProblemDetails Problem) MapUnexpectedException(Exception exception)
{
logger.LogError(exception, "Unhandled exception encountered.");
var details = new ProblemDetails
{
Title = "An unexpected error occurred.",
Detail = exception.Message,
Type = "https://prefab.dev/problems/unexpected-error",
Status = StatusCodes.Status500InternalServerError
};
return (details.Status.Value, details);
}
}
public static class ExceptionHandlingExtensions
{
public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder app)
{
return app.UseMiddleware<ExceptionHandler>();
}
}