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; }
}