Skip to content
This repository was archived by the owner on Aug 30, 2025. It is now read-only.

Commit 21c2e22

Browse files
committed
refactor: Add new ProblemDetails DTO ErrorProblemDetails extended from ProblemDetails;
BREAKING CHANGE: Change `ProblemDetailsResponse` to `ErrorProblemDetails`
1 parent 0fb082d commit 21c2e22

15 files changed

Lines changed: 513 additions & 96 deletions

src/ApiProblemDetailsFactory.cs

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,17 @@ IOptions<ErrorHandlerOptions> errorHandlerOptions
2222
_errorHandlerOptions = errorHandlerOptions?.Value ?? throw new ArgumentNullException(nameof(errorHandlerOptions));
2323
}
2424

25-
internal ProblemDetailsResponse Create(HttpContext httpContext)
25+
internal ErrorProblemDetails Create(HttpContext httpContext)
2626
{
27-
var result = new ProblemDetailsResponse();
27+
var problemDetails = new ErrorProblemDetails();
2828

29-
result.Status = httpContext.GetStatusCode() ?? 0; // Default value is 0
30-
if(_apiBehaviorOptions.ClientErrorMapping.TryGetValue(result.Status, out var clientErrorData))
31-
{
32-
result.Type = clientErrorData.Link;
33-
result.Title = clientErrorData.Title;
34-
}
35-
else
36-
{
37-
result.Type = ProblemDetailsDefaults.Defaults[0].Link;
38-
result.Title = ProblemDetailsDefaults.Defaults[0].Title;
39-
}
40-
41-
result.Instance = httpContext.GetRequestEndpoint();
42-
43-
result.TraceId = httpContext.GetCorrelationId();
29+
_applyDefaults(httpContext, problemDetails);
4430

45-
return result;
31+
return problemDetails;
4632
}
4733

4834

49-
public ProblemDetailsResponse Create(HttpContext httpContext, IEnumerable<KeyValuePair<string, string>> errors)
35+
public ErrorProblemDetails Create(HttpContext httpContext, IEnumerable<KeyValuePair<string, string>> errors)
5036
{
5137
var result = Create(httpContext);
5238

@@ -62,7 +48,7 @@ public ProblemDetailsResponse Create(HttpContext httpContext, IEnumerable<KeyVal
6248
return result;
6349
}
6450

65-
internal ProblemDetailsResponse Create(ActionContext actionContext)
51+
internal ErrorProblemDetails Create(ActionContext actionContext)
6652
{
6753
var payloadTooLargeError = actionContext.ModelState.CheckPayloadTooLargeAndReturnError();
6854
if(payloadTooLargeError.Count == 1)
@@ -93,7 +79,7 @@ public override ProblemDetails CreateProblemDetails(
9379
string instance = null
9480
)
9581
{
96-
statusCode ??= httpContext.GetStatusCode() ?? 0;
82+
statusCode ??= httpContext.GetStatusCode();
9783

9884
var problemDetails = new ProblemDetails
9985
{
@@ -124,7 +110,7 @@ public override ValidationProblemDetails CreateValidationProblemDetails(
124110
throw new ArgumentNullException(nameof(modelStateDictionary));
125111
}
126112

127-
statusCode ??= httpContext.GetStatusCode() ?? 400;
113+
statusCode ??= httpContext.GetStatusCode();
128114

129115
var problemDetails = new ValidationProblemDetails(modelStateDictionary)
130116
{
@@ -147,6 +133,8 @@ public override ValidationProblemDetails CreateValidationProblemDetails(
147133

148134
private void _applyDefaults(HttpContext httpContext, ProblemDetails problemDetails)
149135
{
136+
problemDetails.Status ??= httpContext.GetStatusCode();
137+
150138
if(_apiBehaviorOptions.ClientErrorMapping.TryGetValue(problemDetails.Status.Value, out var clientErrorData))
151139
{
152140
problemDetails.Type ??= clientErrorData.Link;
@@ -179,7 +167,7 @@ private static void _applyDetails(ProblemDetails problemDetails)
179167
404 => "The entity was not found.",
180168
409 => "The entity already exists.",
181169

182-
_ when problemDetails.Status >= 400 && problemDetails.Status < 500 => "A validation error has occurred.",
170+
_ when problemDetails.Status >= 400 && problemDetails.Status < 500 => "One or more validation errors occurred.",
183171

184172
_ => "An unexpected error has occurred."
185173
};

src/ErrorProblemDetails.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
using Microsoft.AspNetCore.Mvc;
6+
using PowerUtils.AspNetCore.ErrorHandler.Serializers;
7+
8+
namespace PowerUtils.AspNetCore.ErrorHandler
9+
{
10+
[JsonConverter(typeof(ErrorProblemDetailsJsonConverter))]
11+
public class ErrorProblemDetails : ProblemDetails
12+
{
13+
/// <summary>
14+
/// ID generated by the problem to track logs
15+
/// </summary>
16+
[JsonPropertyName("traceId")]
17+
public string TraceId { get; set; }
18+
19+
/// <summary>
20+
/// Error property list
21+
/// </summary>
22+
/// <example>
23+
/// { "Property": "Error" }
24+
/// </example>
25+
public IDictionary<string, string> Errors { get; set; } = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
26+
27+
/// <summary>
28+
/// Initializes a new instance of <see cref="ErrorProblemDetails"/>.
29+
/// </summary>
30+
public ErrorProblemDetails() { }
31+
32+
/// <summary>
33+
/// Serialize problem details to JSON
34+
/// </summary>
35+
public override string ToString()
36+
=> JsonSerializer.Serialize(this);
37+
38+
public static implicit operator string(ErrorProblemDetails problemDetailsResponse)
39+
=> problemDetailsResponse.ToString();
40+
}
41+
}

src/Handlers/ErrorHandlerMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ private Task _handleProblem(HttpContext httpContext)
4646

4747
var problemDetails = _problemDetailsFactory.Create(httpContext);
4848

49-
_logger.LogDebug(problemDetails);
49+
_logger.Debug(problemDetails);
5050

5151
return httpContext.WriteProblemDetailsResponseAsync(problemDetails);
5252
}

src/Handlers/ExceptionHandlerMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal static IApplicationBuilder UseExceptionHandlerMiddleware(this IApplicat
2727
.Features
2828
.Get<IExceptionHandlerFeature>()?.Error;
2929

30-
ProblemDetailsResponse problemDetails;
30+
ErrorProblemDetails problemDetails;
3131
if(exception == null)
3232
{
3333
httpContext.ResetResponse(StatusCodes.Status500InternalServerError);

src/Handlers/ModelStateMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ internal static IServiceCollection AddModelStateMiddleware(this IServiceCollecti
2020

2121

2222
actionContext.HttpContext.ResetResponse();
23-
actionContext.HttpContext.Response.StatusCode = problemDetails.Status;
23+
actionContext.HttpContext.Response.StatusCode = problemDetails.Status ?? ProblemDetailsDefaults.FALLBACK_STATUS_CODE;
2424

2525

2626
return new ObjectResult(problemDetails)

src/HttpContextExtensions.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ internal static string GetRequestEndpoint(this HttpContext httpContext)
6161
* Client error responses ( 400 – 499 )
6262
* Server error responses ( 500 – 599 )
6363
*/
64-
internal static int? GetStatusCode(this HttpContext httpContext)
65-
=> httpContext?.Response?.StatusCode;
64+
internal static int GetStatusCode(this HttpContext httpContext)
65+
=> httpContext?.Response?.StatusCode ?? 0;
6666

6767
internal static bool IsNotSuccess(this HttpContext httpContext)
6868
{
@@ -72,7 +72,7 @@ internal static bool IsNotSuccess(this HttpContext httpContext)
7272
return true;
7373
}
7474

75-
if(statusCode is null)
75+
if(statusCode == 0)
7676
{
7777
return true;
7878
}
@@ -121,11 +121,11 @@ internal static void ResetResponse(this HttpContext httpContext, int statusCode)
121121
httpContext.Response.StatusCode = statusCode;
122122
}
123123

124-
internal static async Task WriteProblemDetailsResponseAsync(this HttpContext httpContext, ProblemDetailsResponse response)
124+
internal static async Task WriteProblemDetailsResponseAsync(this HttpContext httpContext, ErrorProblemDetails response)
125125
{
126126
httpContext.ResetResponse();
127127

128-
httpContext.Response.StatusCode = response.Status; // It is required because was a `ResetResponse()`
128+
httpContext.Response.StatusCode = response.Status ?? ProblemDetailsDefaults.FALLBACK_STATUS_CODE; // It is required because was a `ResetResponse()`
129129

130130
// Serialize the problem details object to the Response as JSON (using System.Text.Json)
131131
await JsonSerializer.SerializeAsync(httpContext.Response.Body, response);

src/LoggerExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace PowerUtils.AspNetCore.ErrorHandler
66
{
77
internal static class LoggerExtensions
88
{
9-
public static void Error(this ILogger logger, Exception exception, string request, int statusCode)
9+
public static void Error(this ILogger logger, Exception exception, string request, int? statusCode)
1010
=> logger.LogError(
1111
exception,
1212
$"[ERROR HANDLER] > Request: '{request}', StatusCode: '{statusCode}'"

src/ProblemDetailsDefaults.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace PowerUtils.AspNetCore.ErrorHandler
55
internal static class ProblemDetailsDefaults
66
{
77
public const string PROBLEM_MEDIA_TYPE_JSON = "application/problem+json";
8+
public const int FALLBACK_STATUS_CODE = 500;
89

910
public static readonly Dictionary<int, (string Title, string Link)> Defaults = new()
1011
{

src/ProblemDetailsResponse.cs

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)