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

Commit bfeac44

Browse files
committed
feat: Create IProblemFactory
BREAKING CHANGE: Change `ApiProblemDetailsFactory` from public to internal
1 parent d13a56d commit bfeac44

9 files changed

Lines changed: 387 additions & 16 deletions

File tree

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- [Configure](#ErrorHandler.Configure)
2121
- [PropertyNamingPolicy](#ErrorHandler.PropertyNamingPolicy)
2222
- [ExceptionMappers](#ErrorHandler.ExceptionMappers)
23+
- [IProblemFactory](#ErrorHandler.IProblemFactory)
2324
- [Customize problem link and problem title](#ErrorHandler.CustomizeLinkAndTitle)
2425
- [Add new custom status code](#ErrorHandler.CustomizeLinkAndTitle.AddNew)
2526
- [Change link and title for a specific status code](#ErrorHandler.CustomizeLinkAndTitle.Change)
@@ -115,6 +116,56 @@ public class Startup
115116
```
116117

117118

119+
#### IProblemFactory <a name="ErrorHandler.IProblemFactory"></a>
120+
How to create a custom error problem details for example in a controller
121+
122+
```csharp
123+
[ApiController]
124+
[Route("home")]
125+
public class HomeController : ControllerBase
126+
{
127+
private readonly IProblemFactory _problemFactory;
128+
129+
public ProblemFactoryController(IProblemFactory problemFactory)
130+
=> _problemFactory = problemFactory;
131+
132+
133+
134+
[HttpGet("call-1")]
135+
public IActionResult Call1()
136+
=> _problemFactory.CreateProblemResult(
137+
detail: "detail",
138+
instance: "instance",
139+
statusCode: (int)HttpStatusCode.BadRequest,
140+
title: "title",
141+
type: "type",
142+
errors: new Dictionary<string, string>
143+
{
144+
["Property1"] = "Error1",
145+
["Property2"] = "Error2",
146+
["Property3"] = "Error3",
147+
}
148+
);
149+
150+
[HttpGet("call-2")]
151+
public IActionResult Call2()
152+
=> new ObjectResult(_problemFactory.CreateProblem(
153+
detail: "detail",
154+
instance: "instance",
155+
statusCode: (int)HttpStatusCode.BadRequest,
156+
title: "title",
157+
type: "type",
158+
errors: new Dictionary<string, string>
159+
{
160+
["Property1"] = "Error1",
161+
["Property2"] = "Error2",
162+
["Property3"] = "Error3",
163+
}
164+
));
165+
}
166+
```
167+
168+
118169
#### Customize problem link and problem title <a name="ErrorHandler.CustomizeLinkAndTitle"></a>
119170
Exception mapping to status code and error codes
120171

samples/PowerUtils.AspNetCore.ErrorHandler.Samples/Controllers/GeneralController.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class GeneralController : ControllerBase
1212
public GeneralController(IOptions<ApiBehaviorOptions> options)
1313
=> _options = options;
1414

15+
1516
[HttpGet("version")]
1617
public string GetVersion()
1718
=> Environment.Version.ToString();
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System.Collections.Generic;
2+
using System.Net;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc;
5+
6+
namespace PowerUtils.AspNetCore.ErrorHandler.Samples.Controllers
7+
{
8+
[ApiController]
9+
[Route("problem-details")]
10+
public class ProblemDetailsController : ControllerBase
11+
{
12+
[HttpGet("base-with-object-result")]
13+
public IActionResult GetProblemDetailsWithObjectResult()
14+
{
15+
var result = new ProblemDetails
16+
{
17+
Type = "some url",
18+
Title = "some title",
19+
Status = (int)HttpStatusCode.BadRequest,
20+
};
21+
22+
return new ObjectResult(result);
23+
}
24+
25+
[HttpGet("error-problem-details-with-object-result")]
26+
public IActionResult GetErrorProblemDetailsWithObjectResult()
27+
{
28+
var result = new ErrorProblemDetails
29+
{
30+
Type = "some url",
31+
Title = "some title",
32+
Status = (int)HttpStatusCode.Forbidden,
33+
};
34+
35+
return new ObjectResult(result);
36+
}
37+
38+
[HttpGet("base-problem")]
39+
public IActionResult GetProblem()
40+
=> Problem(statusCode: 409);
41+
42+
43+
#if NET6_0_OR_GREATER
44+
[HttpGet("result-problem")]
45+
public IActionResult GetResultProblem()
46+
=> new ObjectResult(
47+
Results.Problem(
48+
statusCode: 409,
49+
extensions: new Dictionary<string, object>
50+
{
51+
["Errors"] = "fake struct"
52+
}
53+
)
54+
);
55+
#endif
56+
57+
[HttpGet("error-problem")]
58+
public IActionResult GetErrorProblem()
59+
=> new ObjectResult(
60+
new ErrorProblemDetails
61+
{
62+
Status = 409,
63+
Errors = new Dictionary<string, string>()
64+
{
65+
["Errors"] = "fake struct"
66+
}
67+
}
68+
);
69+
}
70+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Collections.Generic;
2+
using System.Net;
3+
using Microsoft.AspNetCore.Mvc;
4+
5+
namespace PowerUtils.AspNetCore.ErrorHandler.Samples.Controllers
6+
{
7+
[ApiController]
8+
[Route("problem-factory")]
9+
public class ProblemFactoryController : ControllerBase
10+
{
11+
private readonly IProblemFactory _problemFactory;
12+
public ProblemFactoryController(IProblemFactory problemFactory)
13+
=> _problemFactory = problemFactory;
14+
15+
16+
17+
[HttpGet("create-result")]
18+
public IActionResult CreateProblemResult()
19+
=> _problemFactory.CreateProblemResult(
20+
detail: "some detail",
21+
instance: "some instance",
22+
statusCode: (int)HttpStatusCode.Forbidden,
23+
title: "some title",
24+
type: "some type",
25+
errors: new Dictionary<string, string>
26+
{
27+
["Key4"] = "Error4",
28+
["Key14"] = "Error124",
29+
}
30+
);
31+
32+
[HttpGet("create-problem")]
33+
public IActionResult CreateProblem()
34+
=> new ObjectResult(_problemFactory.CreateProblem(
35+
detail: "fake detail",
36+
instance: "fake instance",
37+
statusCode: (int)HttpStatusCode.TooManyRequests,
38+
title: "fake title",
39+
type: "fake type",
40+
errors: new Dictionary<string, string>
41+
{
42+
["Key100"] = "Error114",
43+
["Key114"] = "Error11124",
44+
["me"] = "ti"
45+
}
46+
));
47+
}
48+
}

src/ApiProblemDetailsFactory.cs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,66 @@
88

99
namespace PowerUtils.AspNetCore.ErrorHandler
1010
{
11-
public sealed class ApiProblemDetailsFactory : ProblemDetailsFactory
11+
internal sealed class ApiProblemDetailsFactory : ProblemDetailsFactory, IProblemFactory
1212
{
13+
private readonly IHttpContextAccessor _httpContextAccessor;
14+
1315
private readonly ApiBehaviorOptions _apiBehaviorOptions;
1416
private readonly ErrorHandlerOptions _errorHandlerOptions;
1517

1618
public ApiProblemDetailsFactory(
19+
IHttpContextAccessor httpContextAccessor,
1720
IOptions<ApiBehaviorOptions> apiBehaviorOptions,
1821
IOptions<ErrorHandlerOptions> errorHandlerOptions
1922
)
2023
{
24+
_httpContextAccessor = httpContextAccessor;
25+
2126
_apiBehaviorOptions = apiBehaviorOptions?.Value ?? throw new ArgumentNullException(nameof(apiBehaviorOptions));
2227
_errorHandlerOptions = errorHandlerOptions?.Value ?? throw new ArgumentNullException(nameof(errorHandlerOptions));
2328
}
2429

30+
31+
public ObjectResult CreateProblemResult(
32+
string detail = null,
33+
string instance = null,
34+
int? statusCode = null,
35+
string title = null,
36+
string type = null,
37+
IDictionary<string, string> errors = null
38+
) => new ObjectResult(CreateProblem(
39+
detail,
40+
instance,
41+
statusCode,
42+
title,
43+
type,
44+
errors
45+
));
46+
47+
public ErrorProblemDetails CreateProblem(
48+
string detail = null,
49+
string instance = null,
50+
int? statusCode = null,
51+
string title = null,
52+
string type = null,
53+
IDictionary<string, string> errors = null
54+
)
55+
{
56+
var problemDetails = new ErrorProblemDetails
57+
{
58+
Status = statusCode,
59+
Title = title,
60+
Type = type,
61+
Detail = detail,
62+
Instance = instance,
63+
Errors = errors
64+
};
65+
66+
_applyDefaults(_httpContextAccessor.HttpContext, problemDetails);
67+
68+
return problemDetails;
69+
}
70+
2571
internal ErrorProblemDetails Create(HttpContext httpContext)
2672
{
2773
var problemDetails = new ErrorProblemDetails();
@@ -31,7 +77,6 @@ internal ErrorProblemDetails Create(HttpContext httpContext)
3177
return problemDetails;
3278
}
3379

34-
3580
public ErrorProblemDetails Create(HttpContext httpContext, IEnumerable<KeyValuePair<string, string>> errors)
3681
{
3782
var result = Create(httpContext);
@@ -146,14 +191,14 @@ private void _applyDefaults(HttpContext httpContext, ProblemDetails problemDetai
146191
problemDetails.Title ??= ProblemDetailsDefaults.Defaults[0].Title;
147192
}
148193

149-
problemDetails.Instance = httpContext.GetRequestEndpoint();
194+
problemDetails.Instance ??= httpContext.GetRequestEndpoint();
150195

151196
problemDetails.Extensions["traceId"] = httpContext.GetCorrelationId();
152197

153-
_applyDetails(problemDetails);
198+
_applyDetail(problemDetails);
154199
}
155200

156-
private static void _applyDetails(ProblemDetails problemDetails)
201+
private static void _applyDetail(ProblemDetails problemDetails)
157202
{
158203
if(!string.IsNullOrWhiteSpace(problemDetails.Detail))
159204
{

src/ErrorHandlerExtensions.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.AspNetCore.Http;
34
using Microsoft.AspNetCore.Mvc;
45
using Microsoft.AspNetCore.Mvc.Infrastructure;
56
using Microsoft.Extensions.DependencyInjection;
@@ -52,11 +53,14 @@ public static IServiceCollection AddErrorHandler(this IServiceCollection service
5253
services.Configure(options);
5354
}
5455

55-
// Override existent ProblemDetailsFactory
56+
// Override existent (default) ProblemDetailsFactory
5657
services.RemoveAll(typeof(ProblemDetailsFactory));
5758
services.AddSingleton<ProblemDetailsFactory, ApiProblemDetailsFactory>();
5859

59-
services.AddSingleton<ApiProblemDetailsFactory>(); // To can resolve diracly by `ApiProblemDetailsFactory`. Try to use only `ProblemDetailsFactory`
60+
services.AddSingleton<ApiProblemDetailsFactory>(); // To can resolve diracly by `ApiProblemDetailsFactory`
61+
62+
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); // Mandatory to Problem Factory IProblem Factory works
63+
services.AddSingleton<IProblemFactory, ApiProblemDetailsFactory>(); // To be able to use in controllers
6064

6165
services.AddModelStateMiddleware();
6266

src/IProblemHandler.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Collections.Generic;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace PowerUtils.AspNetCore.ErrorHandler
5+
{
6+
public interface IProblemFactory
7+
{
8+
/// <summary>
9+
/// Produces a <see cref="ObjectResult"/> response.
10+
/// </summary>
11+
/// <param name="statusCode">The value for <see cref="ProblemDetails.Status" />.</param>
12+
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
13+
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
14+
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
15+
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
16+
/// <param name="errors">The value for <see cref="ErrorProblemDetails.Errors" />.</param>
17+
/// <returns>The created <see cref="ObjectResult"/> for the response.</returns>
18+
ObjectResult CreateProblemResult(
19+
string detail = null,
20+
string instance = null,
21+
int? statusCode = null,
22+
string title = null,
23+
string type = null,
24+
IDictionary<string, string> errors = null
25+
);
26+
27+
/// <summary>
28+
/// Produces a <see cref="ErrorProblemDetails"/> response.
29+
/// </summary>
30+
/// <param name="statusCode">The value for <see cref="ProblemDetails.Status" />.</param>
31+
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
32+
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
33+
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
34+
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
35+
/// <param name="errors">The value for <see cref="ErrorProblemDetails.Errors" />.</param>
36+
/// <returns>The created <see cref="ErrorProblemDetails"/> for the response.</returns>
37+
ErrorProblemDetails CreateProblem(
38+
string detail = null,
39+
string instance = null,
40+
int? statusCode = null,
41+
string title = null,
42+
string type = null,
43+
IDictionary<string, string> errors = null
44+
);
45+
}
46+
}

0 commit comments

Comments
 (0)