Skip to content

Commit 8e8f88a

Browse files
committed
Add a factory
1 parent ac108b6 commit 8e8f88a

11 files changed

+548
-209
lines changed

src/Mvc/Mvc.Core/src/ControllerBase.cs

Lines changed: 60 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public abstract class ControllerBase
3232
private IModelBinderFactory _modelBinderFactory;
3333
private IObjectModelValidator _objectValidator;
3434
private IUrlHelper _url;
35+
private ProblemDetailsFactory _problemDetailsFactory;
3536

3637
/// <summary>
3738
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
@@ -190,6 +191,28 @@ public IObjectModelValidator ObjectValidator
190191
}
191192
}
192193

194+
public ProblemDetailsFactory ProblemDetailsFactory
195+
{
196+
get
197+
{
198+
if (_problemDetailsFactory == null)
199+
{
200+
_problemDetailsFactory = HttpContext?.RequestServices?.GetRequiredService<ProblemDetailsFactory>();
201+
}
202+
203+
return _problemDetailsFactory;
204+
}
205+
set
206+
{
207+
if (value == null)
208+
{
209+
throw new ArgumentNullException(nameof(value));
210+
}
211+
212+
_problemDetailsFactory = value;
213+
}
214+
}
215+
193216
/// <summary>
194217
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
195218
/// </summary>
@@ -1823,51 +1846,33 @@ public virtual ConflictObjectResult Conflict([ActionResultObjectValue] ModelStat
18231846
=> new ConflictObjectResult(modelState);
18241847

18251848
/// <summary>
1826-
/// Creates an <see cref="ObjectResult"/> that produces a <see cref="ProblemDetails"/> response with a <c>500</c>
1827-
/// error status with a <see cref="ProblemDetails" /> value.
1849+
/// Creates an <see cref="ObjectResult"/> that produces a <see cref="ProblemDetails"/> response.
18281850
/// </summary>
1829-
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
1830-
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
1851+
/// <param name="statusCode">The value for <see cref="ProblemDetails.Status" />..</param>
18311852
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
18321853
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
1854+
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
1855+
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
18331856
/// <returns>The created <see cref="ObjectResult"/> for the response.</returns>
18341857
[NonAction]
18351858
public virtual ObjectResult Problem(
18361859
string detail = null,
18371860
string instance = null,
1861+
int? statusCode = null,
18381862
string title = null,
18391863
string type = null)
18401864
{
1841-
var problemDetails = new ProblemDetails
1842-
{
1843-
Title = title,
1844-
Type = type,
1845-
Detail = detail,
1846-
Instance = instance,
1847-
};
1848-
1849-
ApplyProblemDetailsDefaults(problemDetails, statusCode: 500);
1865+
var problemDetails = ProblemDetailsFactory.CreateProblemDetails(
1866+
HttpContext,
1867+
statusCode: statusCode ?? 500,
1868+
title: title,
1869+
type: type,
1870+
detail: detail,
1871+
instance: instance);
18501872

18511873
return new ObjectResult(problemDetails);
18521874
}
18531875

1854-
private void ApplyProblemDetailsDefaults(ProblemDetails problemDetails, int statusCode)
1855-
{
1856-
problemDetails.Status = statusCode;
1857-
1858-
if (problemDetails.Title is null || problemDetails.Type is null)
1859-
{
1860-
var options = HttpContext.RequestServices.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
1861-
if (options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData))
1862-
{
1863-
problemDetails.Title ??= clientErrorData.Title;
1864-
problemDetails.Type ??= clientErrorData.Link;
1865-
}
1866-
}
1867-
1868-
ProblemDetailsClientErrorFactory.SetTraceId(ControllerContext, problemDetails);
1869-
}
1870-
18711876
/// <summary>
18721877
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response.
18731878
/// </summary>
@@ -1884,70 +1889,64 @@ public virtual ActionResult ValidationProblem([ActionResultObjectValue] Validati
18841889
}
18851890

18861891
/// <summary>
1887-
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
1892+
/// Creates an <see cref="ActionResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
18881893
/// with validation errors from <paramref name="modelStateDictionary"/>.
18891894
/// </summary>
18901895
/// <param name="modelStateDictionary">The <see cref="ModelStateDictionary"/>.</param>
18911896
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
18921897
[NonAction]
18931898
public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary)
1894-
{
1895-
if (modelStateDictionary == null)
1896-
{
1897-
throw new ArgumentNullException(nameof(modelStateDictionary));
1898-
}
1899+
=> ValidationProblem(detail: null, modelStateDictionary: modelStateDictionary);
18991900

1900-
var validationProblem = new ValidationProblemDetails(modelStateDictionary);
1901-
ApplyProblemDetailsDefaults(validationProblem, statusCode: 400);
1902-
1903-
return new BadRequestObjectResult(validationProblem);
1904-
}
19051901

19061902
/// <summary>
1907-
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
1903+
/// Creates an <see cref="ActionResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
19081904
/// with validation errors from <see cref="ModelState"/>.
19091905
/// </summary>
1910-
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
1906+
/// <returns>The created <see cref="ActionResult"/> for the response.</returns>
19111907
[NonAction]
1912-
public virtual ActionResult ValidationProblem() => ValidationProblem(ModelState);
1908+
public virtual ActionResult ValidationProblem()
1909+
=> ValidationProblem(ModelState);
19131910

19141911
/// <summary>
1915-
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
1912+
/// Creates an <see cref="ActionResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
19161913
/// with a <see cref="ValidationProblemDetails"/> value.
19171914
/// </summary>
1918-
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
1919-
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
19201915
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
19211916
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
1917+
/// <param name="statusCode">The status code.</param>
1918+
/// <param name="title">The value for <see cref="ProblemDetails.Title" />.</param>
1919+
/// <param name="type">The value for <see cref="ProblemDetails.Type" />.</param>
19221920
/// <param name="modelStateDictionary">The <see cref="ModelStateDictionary"/>.
19231921
/// When <see langword="null"/> uses <see cref="ModelState"/>.</param>
1924-
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
1922+
/// <returns>The created <see cref="ActionResult"/> for the response.</returns>
19251923
[NonAction]
19261924
public virtual ActionResult ValidationProblem(
1927-
string detail,
1925+
string detail = null,
19281926
string instance = null,
1927+
int? statusCode = null,
19291928
string title = null,
19301929
string type = null,
19311930
[ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null)
19321931
{
19331932
modelStateDictionary ??= ModelState;
19341933

1935-
var validationProblem = new ValidationProblemDetails(modelStateDictionary)
1936-
{
1937-
Detail = detail,
1938-
Instance = instance,
1939-
Type = type,
1940-
};
1934+
var validationProblem = ProblemDetailsFactory.CreateValidationProblemDetails(
1935+
HttpContext,
1936+
modelStateDictionary,
1937+
statusCode: statusCode,
1938+
title: title,
1939+
type: type,
1940+
detail: detail,
1941+
instance: instance);
19411942

1942-
if (title != null)
1943+
if (validationProblem.Status == 400)
19431944
{
1944-
// ValidationProblemDetails has a Title by default. Do not overwrite it with a null
1945-
validationProblem.Title = title;
1945+
// For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400.
1946+
return new BadRequestObjectResult(validationProblem);
19461947
}
19471948

1948-
ApplyProblemDetailsDefaults(validationProblem, statusCode: 400);
1949-
1950-
return new BadRequestObjectResult(validationProblem);
1949+
return new ObjectResult(validationProblem);
19511950
}
19521951

19531952
/// <summary>

src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using Microsoft.AspNetCore.Http;
65
using Microsoft.AspNetCore.Mvc;
76
using Microsoft.AspNetCore.Mvc.Core;
87
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -12,37 +11,43 @@ namespace Microsoft.Extensions.DependencyInjection
1211
{
1312
internal class ApiBehaviorOptionsSetup : IConfigureOptions<ApiBehaviorOptions>
1413
{
14+
private ProblemDetailsFactory _problemDetailsFactory;
15+
1516
public void Configure(ApiBehaviorOptions options)
1617
{
1718
if (options == null)
1819
{
1920
throw new ArgumentNullException(nameof(options));
2021
}
2122

22-
options.InvalidModelStateResponseFactory = ProblemDetailsInvalidModelStateResponse;
23-
ConfigureClientErrorMapping(options);
24-
25-
IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context)
23+
options.InvalidModelStateResponseFactory = context =>
2624
{
27-
var problemDetails = new ValidationProblemDetails(context.ModelState)
28-
{
29-
Status = StatusCodes.Status400BadRequest,
30-
};
31-
32-
if (options.ClientErrorMapping.TryGetValue(400, out var clientErrorData))
33-
{
34-
problemDetails.Type = clientErrorData.Link;
35-
}
36-
37-
ProblemDetailsClientErrorFactory.SetTraceId(context, problemDetails);
38-
39-
var result = new BadRequestObjectResult(problemDetails);
25+
// ProblemDetailsFactory depends on the ApiBehaviorOptions instance. We intentionally avoid constructor injecting
26+
// it in this options setup to to avoid a DI cycle.
27+
_problemDetailsFactory ??= context.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
28+
return ProblemDetailsInvalidModelStateResponse(_problemDetailsFactory, context);
29+
};
4030

41-
result.ContentTypes.Add("application/problem+json");
42-
result.ContentTypes.Add("application/problem+xml");
31+
ConfigureClientErrorMapping(options);
32+
}
4333

44-
return result;
34+
internal static IActionResult ProblemDetailsInvalidModelStateResponse(ProblemDetailsFactory problemDetailsFactory, ActionContext context)
35+
{
36+
var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState);
37+
ObjectResult result;
38+
if (problemDetails.Status == 400)
39+
{
40+
// For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400.
41+
result = new BadRequestObjectResult(problemDetails);
4542
}
43+
else
44+
{
45+
result = new ObjectResult(problemDetails);
46+
}
47+
result.ContentTypes.Add("application/problem+json");
48+
result.ContentTypes.Add("application/problem+xml");
49+
50+
return result;
4651
}
4752

4853
// Internal for unit testing

src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ internal static void AddMvcCoreServices(IServiceCollection services)
258258
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
259259
services.TryAddSingleton<IActionResultExecutor<JsonResult>, SystemTextJsonResultExecutor>();
260260
services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
261+
services.TryAddSingleton<ProblemDetailsFactory, DefaultProblemDetailsFactory>();
261262

262263
//
263264
// Route Handlers
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Mvc.ModelBinding;
8+
using Microsoft.Extensions.Options;
9+
10+
namespace Microsoft.AspNetCore.Mvc.Infrastructure
11+
{
12+
internal sealed class DefaultProblemDetailsFactory : ProblemDetailsFactory
13+
{
14+
private readonly ApiBehaviorOptions _options;
15+
16+
public DefaultProblemDetailsFactory(IOptions<ApiBehaviorOptions> options)
17+
{
18+
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
19+
}
20+
21+
public override ProblemDetails CreateProblemDetails(
22+
HttpContext httpContext,
23+
int? statusCode = null,
24+
string title = null,
25+
string type = null,
26+
string detail = null,
27+
string instance = null)
28+
{
29+
statusCode ??= 500;
30+
31+
var problemDetails = new ProblemDetails
32+
{
33+
Status = statusCode,
34+
Title = title,
35+
Type = type,
36+
Detail = detail,
37+
Instance = instance,
38+
};
39+
40+
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
41+
42+
return problemDetails;
43+
}
44+
45+
public override ValidationProblemDetails CreateValidationProblemDetails(
46+
HttpContext httpContext,
47+
ModelStateDictionary modelStateDictionary,
48+
int? statusCode = null,
49+
string title = null,
50+
string type = null,
51+
string detail = null,
52+
string instance = null)
53+
{
54+
if (modelStateDictionary == null)
55+
{
56+
throw new ArgumentNullException(nameof(modelStateDictionary));
57+
}
58+
59+
statusCode ??= 400;
60+
61+
var problemDetails = new ValidationProblemDetails(modelStateDictionary)
62+
{
63+
Status = statusCode,
64+
Type = type,
65+
Detail = detail,
66+
Instance = instance,
67+
};
68+
69+
if (title != null)
70+
{
71+
// For validation problem details, don't overwrite the default title with null.
72+
problemDetails.Title = title;
73+
}
74+
75+
ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);
76+
77+
return problemDetails;
78+
}
79+
80+
private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode)
81+
{
82+
problemDetails.Status ??= statusCode;
83+
84+
if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData))
85+
{
86+
problemDetails.Title ??= clientErrorData.Title;
87+
problemDetails.Type ??= clientErrorData.Link;
88+
}
89+
90+
var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
91+
if (traceId != null)
92+
{
93+
problemDetails.Extensions["traceId"] = traceId;
94+
}
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)