Skip to content

Commit ac108b6

Browse files
committed
Add helper methods on ControllerBase to return ProblemDetails
* Introduce ControllerBase.Problem and ValidationProblem overload that accepts optional parameters * Consistently use ClientErrorData when generating ProblemDetails * Clean-up InvalidModelStateResponseFactory initialization. Fixes #8537
1 parent 447205c commit ac108b6

10 files changed

+343
-140
lines changed

src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs

+4
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,8 @@ protected ControllerBase() { }
445445
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
446446
public virtual Microsoft.AspNetCore.Mvc.PhysicalFileResult PhysicalFile(string physicalPath, string contentType, string fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue entityTag, bool enableRangeProcessing) { throw null; }
447447
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
448+
public virtual Microsoft.AspNetCore.Mvc.ObjectResult Problem(string detail = null, string instance = null, string title = null, string type = null) { throw null; }
449+
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
448450
public virtual Microsoft.AspNetCore.Mvc.RedirectResult Redirect(string url) { throw null; }
449451
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
450452
public virtual Microsoft.AspNetCore.Mvc.RedirectResult RedirectPermanent(string url) { throw null; }
@@ -586,6 +588,8 @@ protected ControllerBase() { }
586588
public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary) { throw null; }
587589
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
588590
public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ValidationProblemDetails descriptor) { throw null; }
591+
[Microsoft.AspNetCore.Mvc.NonActionAttribute]
592+
public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem(string detail, string instance = null, string title = null, string type = null, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary = null) { throw null; }
589593
}
590594
public partial class ControllerContext : Microsoft.AspNetCore.Mvc.ActionContext
591595
{

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

+89-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Microsoft.AspNetCore.Mvc.Routing;
1717
using Microsoft.AspNetCore.Routing;
1818
using Microsoft.Extensions.DependencyInjection;
19+
using Microsoft.Extensions.Options;
1920
using Microsoft.Net.Http.Headers;
2021

2122
namespace Microsoft.AspNetCore.Mvc
@@ -1821,6 +1822,52 @@ public virtual ConflictObjectResult Conflict([ActionResultObjectValue] object er
18211822
public virtual ConflictObjectResult Conflict([ActionResultObjectValue] ModelStateDictionary modelState)
18221823
=> new ConflictObjectResult(modelState);
18231824

1825+
/// <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.
1828+
/// </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>
1831+
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
1832+
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
1833+
/// <returns>The created <see cref="ObjectResult"/> for the response.</returns>
1834+
[NonAction]
1835+
public virtual ObjectResult Problem(
1836+
string detail = null,
1837+
string instance = null,
1838+
string title = null,
1839+
string type = null)
1840+
{
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);
1850+
1851+
return new ObjectResult(problemDetails);
1852+
}
1853+
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+
18241871
/// <summary>
18251872
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response.
18261873
/// </summary>
@@ -1837,8 +1884,10 @@ public virtual ActionResult ValidationProblem([ActionResultObjectValue] Validati
18371884
}
18381885

18391886
/// <summary>
1840-
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response.
1887+
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
1888+
/// with validation errors from <paramref name="modelStateDictionary"/>.
18411889
/// </summary>
1890+
/// <param name="modelStateDictionary">The <see cref="ModelStateDictionary"/>.</param>
18421891
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
18431892
[NonAction]
18441893
public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary)
@@ -1849,6 +1898,8 @@ public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelSta
18491898
}
18501899

18511900
var validationProblem = new ValidationProblemDetails(modelStateDictionary);
1901+
ApplyProblemDetailsDefaults(validationProblem, statusCode: 400);
1902+
18521903
return new BadRequestObjectResult(validationProblem);
18531904
}
18541905

@@ -1858,9 +1909,44 @@ public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelSta
18581909
/// </summary>
18591910
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
18601911
[NonAction]
1861-
public virtual ActionResult ValidationProblem()
1912+
public virtual ActionResult ValidationProblem() => ValidationProblem(ModelState);
1913+
1914+
/// <summary>
1915+
/// Creates an <see cref="BadRequestObjectResult"/> that produces a <see cref="StatusCodes.Status400BadRequest"/> response
1916+
/// with a <see cref="ValidationProblemDetails"/> value.
1917+
/// </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>
1920+
/// <param name="detail">The value for <see cref="ProblemDetails.Detail" />.</param>
1921+
/// <param name="instance">The value for <see cref="ProblemDetails.Instance" />.</param>
1922+
/// <param name="modelStateDictionary">The <see cref="ModelStateDictionary"/>.
1923+
/// When <see langword="null"/> uses <see cref="ModelState"/>.</param>
1924+
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
1925+
[NonAction]
1926+
public virtual ActionResult ValidationProblem(
1927+
string detail,
1928+
string instance = null,
1929+
string title = null,
1930+
string type = null,
1931+
[ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null)
18621932
{
1863-
var validationProblem = new ValidationProblemDetails(ModelState);
1933+
modelStateDictionary ??= ModelState;
1934+
1935+
var validationProblem = new ValidationProblemDetails(modelStateDictionary)
1936+
{
1937+
Detail = detail,
1938+
Instance = instance,
1939+
Type = type,
1940+
};
1941+
1942+
if (title != null)
1943+
{
1944+
// ValidationProblemDetails has a Title by default. Do not overwrite it with a null
1945+
validationProblem.Title = title;
1946+
}
1947+
1948+
ApplyProblemDetailsDefaults(validationProblem, statusCode: 400);
1949+
18641950
return new BadRequestObjectResult(validationProblem);
18651951
}
18661952

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

+24-41
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,38 @@
1010

1111
namespace Microsoft.Extensions.DependencyInjection
1212
{
13-
internal class ApiBehaviorOptionsSetup :
14-
IConfigureOptions<ApiBehaviorOptions>,
15-
IPostConfigureOptions<ApiBehaviorOptions>
13+
internal class ApiBehaviorOptionsSetup : IConfigureOptions<ApiBehaviorOptions>
1614
{
17-
internal static readonly Func<ActionContext, IActionResult> DefaultFactory = DefaultInvalidModelStateResponse;
18-
internal static readonly Func<ActionContext, IActionResult> ProblemDetailsFactory =
19-
ProblemDetailsInvalidModelStateResponse;
20-
2115
public void Configure(ApiBehaviorOptions options)
2216
{
2317
if (options == null)
2418
{
2519
throw new ArgumentNullException(nameof(options));
2620
}
2721

28-
options.InvalidModelStateResponseFactory = DefaultFactory;
22+
options.InvalidModelStateResponseFactory = ProblemDetailsInvalidModelStateResponse;
2923
ConfigureClientErrorMapping(options);
30-
}
3124

32-
public void PostConfigure(string name, ApiBehaviorOptions options)
33-
{
34-
// We want to use problem details factory only if
35-
// (a) it has not been opted out of (SuppressMapClientErrors = true)
36-
// (b) a different factory was configured
37-
if (!options.SuppressMapClientErrors &&
38-
object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory))
25+
IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context)
3926
{
40-
options.InvalidModelStateResponseFactory = ProblemDetailsFactory;
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);
40+
41+
result.ContentTypes.Add("application/problem+json");
42+
result.ContentTypes.Add("application/problem+xml");
43+
44+
return result;
4145
}
4246
}
4347

@@ -91,33 +95,12 @@ internal static void ConfigureClientErrorMapping(ApiBehaviorOptions options)
9195
Link = "https://tools.ietf.org/html/rfc4918#section-11.2",
9296
Title = Resources.ApiConventions_Title_422,
9397
};
94-
}
9598

96-
private static IActionResult DefaultInvalidModelStateResponse(ActionContext context)
97-
{
98-
var result = new BadRequestObjectResult(context.ModelState);
99-
100-
result.ContentTypes.Add("application/json");
101-
result.ContentTypes.Add("application/xml");
102-
103-
return result;
104-
}
105-
106-
internal static IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context)
107-
{
108-
var problemDetails = new ValidationProblemDetails(context.ModelState)
99+
options.ClientErrorMapping[500] = new ClientErrorData
109100
{
110-
Status = StatusCodes.Status400BadRequest,
101+
Link = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
102+
Title = Resources.ApiConventions_Title_500,
111103
};
112-
113-
ProblemDetailsClientErrorFactory.SetTraceId(context, problemDetails);
114-
115-
var result = new BadRequestObjectResult(problemDetails);
116-
117-
result.ContentTypes.Add("application/problem+json");
118-
result.ContentTypes.Add("application/problem+xml");
119-
120-
return result;
121104
}
122105
}
123106
}

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

-2
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,6 @@ internal static void AddMvcCoreServices(IServiceCollection services)
149149
ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcCoreMvcOptionsSetup>());
150150
services.TryAddEnumerable(
151151
ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
152-
services.TryAddEnumerable(
153-
ServiceDescriptor.Transient<IPostConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
154152
services.TryAddEnumerable(
155153
ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, MvcCoreRouteOptionsSetup>());
156154

src/Mvc/Mvc.Core/src/Resources.resx

+31-28
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -507,4 +507,7 @@
507507
<data name="ObjectResultExecutor_MaxEnumerationExceeded" xml:space="preserve">
508508
<value>'{0}' reached the configured maximum size of the buffer when enumerating a value of type '{1}'. This limit is in place to prevent infinite streams of 'IAsyncEnumerable&lt;&gt;' from continuing indefinitely. If this is not a programming mistake, consider ways to reduce the collection size, or consider manually converting '{1}' into a list rather than increasing the limit.</value>
509509
</data>
510-
</root>
510+
<data name="ApiConventions_Title_500" xml:space="preserve">
511+
<value>An error occured while processing your request.</value>
512+
</data>
513+
</root>

0 commit comments

Comments
 (0)