From 28ed566854dc509fbcac20a06a431325f41111f2 Mon Sep 17 00:00:00 2001 From: Ken Swan <49496839+kenswan@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:08:15 -0500 Subject: [PATCH 1/3] Should Override Problem Details - Test --- .../src/ExceptionsMiddlewareOptions.cs | 6 ++ ...ionBuilderMiddlewareTests.ProblemDetail.cs | 67 +++++++++++++++++-- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/Middleware/src/ExceptionsMiddlewareOptions.cs b/src/Middleware/src/ExceptionsMiddlewareOptions.cs index eaa6caa..9941917 100644 --- a/src/Middleware/src/ExceptionsMiddlewareOptions.cs +++ b/src/Middleware/src/ExceptionsMiddlewareOptions.cs @@ -4,6 +4,7 @@ // ------------------------------------------------------- using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using System.Net; namespace BlazorFocused.Exceptions.Middleware; @@ -32,4 +33,9 @@ public class ExceptionsMiddlewareOptions /// Default error status code if none specified for given type of exception /// public HttpStatusCode DefaultErrorStatusCode { get; set; } = HttpStatusCode.InternalServerError; + + /// + /// Provide a way to override/configure the ProblemDetails object before it is returned to the client + /// + public Func ConfigureProblemDetails { get; init; } } diff --git a/src/Middleware/test/ApplicationBuilder/ApplicationBuilderMiddlewareTests.ProblemDetail.cs b/src/Middleware/test/ApplicationBuilder/ApplicationBuilderMiddlewareTests.ProblemDetail.cs index c66b4ce..e1b1029 100644 --- a/src/Middleware/test/ApplicationBuilder/ApplicationBuilderMiddlewareTests.ProblemDetail.cs +++ b/src/Middleware/test/ApplicationBuilder/ApplicationBuilderMiddlewareTests.ProblemDetail.cs @@ -3,6 +3,8 @@ // Licensed under the MIT License // ------------------------------------------------------- +using BlazorFocused.Exceptions.Middleware.ApplicationBuilder; +using BlazorFocused.Exceptions.Middleware.ExceptionBuilder; using Bogus; using FluentAssertions; using Microsoft.AspNetCore.Http; @@ -11,8 +13,6 @@ using Microsoft.Extensions.Options; using Moq; using System.Net; -using BlazorFocused.Exceptions.Middleware.ApplicationBuilder; -using BlazorFocused.Exceptions.Middleware.ExceptionBuilder; namespace BlazorFocused.Exceptions.Middleware.Test.ApplicationBuilder; @@ -30,7 +30,7 @@ public async Task Invoke_ShouldReturnProblemDetailsNoConfiguration(Exception thr requestDelegateMock.Setup(request => request.Invoke(httpContext)) - .ThrowsAsync(thrownException); + .ThrowsAsync(thrownException); Exception actualException = await Record.ExceptionAsync(() => @@ -54,7 +54,8 @@ await Record.ExceptionAsync(() => [Theory] [MemberData(nameof(ExceptionsWithStatusCode))] - public async Task Invoke_ShouldReturnProblemDetailsWithDefaultMessage(Exception thrownException, HttpStatusCode expectedStatusCode) + public async Task Invoke_ShouldReturnProblemDetailsWithDefaultMessage(Exception thrownException, + HttpStatusCode expectedStatusCode) { using MemoryStream memoryStream = GenerateHttpContext(out string expectedInstance, out HttpContext httpContext); @@ -73,7 +74,7 @@ public async Task Invoke_ShouldReturnProblemDetailsWithDefaultMessage(Exception requestDelegateMock.Setup(request => request.Invoke(httpContext)) - .ThrowsAsync(thrownException); + .ThrowsAsync(thrownException); Exception actualException = await Record.ExceptionAsync(() => @@ -125,7 +126,7 @@ public async Task Invoke_ShouldReturnProblemDetailsWithConfiguredMessage() requestDelegateMock.Setup(request => request.Invoke(httpContext)) - .ThrowsAsync(thrownException); + .ThrowsAsync(thrownException); Exception actualException = await Record.ExceptionAsync(() => @@ -147,4 +148,58 @@ await Record.ExceptionAsync(() => Assert.Equal((int)expectedStatusCode, httpContext.Response.StatusCode); Assert.Equal(thrownException.GetType().Name, actualErrorResponse.Type); } + + [Fact] + public async Task Invoke_ShouldAllowProblemDetailsOverride() + { + string exceptionMessage = new Faker().Lorem.Sentence(); + string expectedType = "Test Override 1"; + string overrideInstance = " - Test Override 2"; + string overrideMessage = "Test Override 3 "; + string expectedDetail = overrideMessage + exceptionMessage; + int expectedStatusCode = (int)HttpStatusCode.GatewayTimeout; + var thrownException = new ApplicationException(exceptionMessage); + + using MemoryStream memoryStream = + GenerateHttpContext(out string initialInstance, out HttpContext httpContext); + + string expectedInstance = initialInstance + overrideInstance; + + IOptionsMonitor optionsMonitor = null; + var exceptionsMiddlewareOptions = new ExceptionsMiddlewareOptions + { + ConfigureProblemDetails = (httpContext, exceptionMessage, problemDetails) => + { + problemDetails.Detail = overrideMessage + exceptionMessage.Message; + problemDetails.Instance = httpContext.Request.Path + httpContext.Request.QueryString + overrideInstance; + problemDetails.Status = expectedStatusCode; + problemDetails.Type = expectedType; + + return problemDetails; + } + }; + + requestDelegateMock.Setup(request => + request.Invoke(httpContext)) + .ThrowsAsync(thrownException); + + Exception actualException = + await Record.ExceptionAsync(() => + applicationBuilderMiddleware.Invoke( + httpContext, + Options.Create(exceptionsMiddlewareOptions), + optionsMonitor, + NullLogger.Instance)); + + ProblemDetails actualErrorResponse = await GetErrorResponseFromBody(memoryStream); + + // Should be null since error is caught + actualException.Should().BeNull(); + + Assert.Equal(expectedDetail, actualErrorResponse.Detail); + Assert.Equal(expectedInstance, actualErrorResponse.Instance); + Assert.Equal(expectedStatusCode, actualErrorResponse.Status); + Assert.Equal(expectedType, actualErrorResponse.Type); + Assert.NotEqual(expectedStatusCode, httpContext.Response.StatusCode); + } } From 2065e5886a080586ecf184b246bf5694a6349b53 Mon Sep 17 00:00:00 2001 From: Ken Swan <49496839+kenswan@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:08:28 -0500 Subject: [PATCH 2/3] Should Override Problem Details - Pass --- .../src/ApplicationBuilder/ApplicationBuilderMiddleware.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Middleware/src/ApplicationBuilder/ApplicationBuilderMiddleware.cs b/src/Middleware/src/ApplicationBuilder/ApplicationBuilderMiddleware.cs index 7f87448..f07e073 100644 --- a/src/Middleware/src/ApplicationBuilder/ApplicationBuilderMiddleware.cs +++ b/src/Middleware/src/ApplicationBuilder/ApplicationBuilderMiddleware.cs @@ -160,6 +160,11 @@ async Task LogAndWriteProblemDetailsExceptionAsync(ProblemDetails problemDetails httpContext.Response.StatusCode = problemDetails.Status ?? (int)HttpStatusCode.InternalServerError; + if (exceptionsMiddlewareOptionsValue.ConfigureProblemDetails is not null) + { + problemDetails = exceptionsMiddlewareOptionsValue.ConfigureProblemDetails(httpContext, exception, problemDetails); + } + await httpContext.Response.WriteAsJsonAsync( problemDetails, problemDetails.GetType(), // WriteAsJson needs type to add additional fields provided in ValidationProblemDetails From 1d04bd673227b1d36262ad1dd4dd6a25f9d1cb63 Mon Sep 17 00:00:00 2001 From: Ken Swan <49496839+kenswan@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:16:44 -0500 Subject: [PATCH 3/3] Add New Implementation to Sample --- samples/MiddlewareSample/MiddlewareSample.Api/Program.cs | 9 ++++++++- src/Middleware/src/ExceptionsMiddlewareOptions.cs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/samples/MiddlewareSample/MiddlewareSample.Api/Program.cs b/samples/MiddlewareSample/MiddlewareSample.Api/Program.cs index ea2551e..cf6a85b 100644 --- a/samples/MiddlewareSample/MiddlewareSample.Api/Program.cs +++ b/samples/MiddlewareSample/MiddlewareSample.Api/Program.cs @@ -15,12 +15,19 @@ // Register custom exceptions and status codes builder.Services - // .AddExceptionsMiddleware() -> Use this extension for default correlation key/value + // .AddExceptionsMiddleware() -> Use this extension for default correlation key/value and configuration .AddExceptionsMiddleware(options => { options.CorrelationKey = "X-TestCorrelation-Id"; options.CorrelationKey = CORRELATION_HEADER_KEY; options.ConfigureCorrelationValue = (httpContext) => httpContext.TraceIdentifier; + + options.ConfigureProblemDetails = (httpContext, exception, problemDetails) => + { + Console.WriteLine("Here you can override the ProblemDetails object returned to the client."); + Console.WriteLine("Also a good place to breakpoint during development to examine thrown exceptions."); + return problemDetails; + }; }) // Use this extension for default correlation key/value .AddException(HttpStatusCode.FailedDependency); diff --git a/src/Middleware/src/ExceptionsMiddlewareOptions.cs b/src/Middleware/src/ExceptionsMiddlewareOptions.cs index 9941917..86a0545 100644 --- a/src/Middleware/src/ExceptionsMiddlewareOptions.cs +++ b/src/Middleware/src/ExceptionsMiddlewareOptions.cs @@ -37,5 +37,5 @@ public class ExceptionsMiddlewareOptions /// /// Provide a way to override/configure the ProblemDetails object before it is returned to the client /// - public Func ConfigureProblemDetails { get; init; } + public Func ConfigureProblemDetails { get; set; } }