Init
This commit is contained in:
229
Prefab.Web/Shared/ExceptionHandler.cs
Normal file
229
Prefab.Web/Shared/ExceptionHandler.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user