Use casing for ProblemDetails that specified by RFC

* Use JsonProperty.MemberName to specify lowercase casing for ProblemDetails properties -
  https://tools.ietf.org/html/rfc7807#section-3
* Use XML NS and lowercase for Xml elements specified by RFC -
  https://tools.ietf.org/html/rfc7807#appendix-A

Fixes https://github.com/aspnet/Mvc/issues/8501
This commit is contained in:
Pranav K 2018-10-01 15:05:56 -07:00
parent d3c8d171bd
commit 164d14064c
8 changed files with 104 additions and 74 deletions

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
/// "about:blank".
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "type")]
public string Type { get; set; }
/// <summary>
@ -26,25 +26,25 @@ namespace Microsoft.AspNetCore.Mvc
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
/// see[RFC7231], Section 3.4).
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "title")]
public string Title { get; set; }
/// <summary>
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "status")]
public int? Status { get; set; }
/// <summary>
/// A human-readable explanation specific to this occurrence of the problem.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "detail")]
public string Detail { get; set; }
/// <summary>
/// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "instance")]
public string Instance { get; set; }
/// <summary>

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Mvc
{
@ -64,6 +65,7 @@ namespace Microsoft.AspNetCore.Mvc
/// <summary>
/// Gets or sets the validation errors associated with this instance of <see cref="ValidationProblemDetails"/>.
/// </summary>
[JsonProperty(PropertyName = "errors")]
public IDictionary<string, string[]> Errors { get; } = new Dictionary<string, string[]>(StringComparer.Ordinal);
}
}

View File

@ -12,9 +12,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
/// <summary>
/// Wrapper class for <see cref="Mvc.ProblemDetails"/> to enable it to be serialized by the xml formatters.
/// </summary>
[XmlRoot(nameof(ProblemDetails))]
[XmlRoot("problem", Namespace = Namespace)]
public class ProblemDetailsWrapper : IXmlSerializable, IUnwrappable
{
internal const string Namespace = "urn:ietf:rfc:7807";
/// <summary>
/// Key used to represent dictionary elements with empty keys
/// </summary>
@ -83,25 +85,25 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
switch (name)
{
case nameof(ProblemDetails.Detail):
case "detail":
ProblemDetails.Detail = value;
break;
case nameof(ProblemDetails.Instance):
case "instance":
ProblemDetails.Instance = value;
break;
case nameof(ProblemDetails.Status):
case "status":
ProblemDetails.Status = string.IsNullOrEmpty(value) ?
(int?)null :
int.Parse(value, CultureInfo.InvariantCulture);
break;
case nameof(ProblemDetails.Title):
case "title":
ProblemDetails.Title = value;
break;
case nameof(ProblemDetails.Type):
case "type":
ProblemDetails.Type = value;
break;
@ -122,20 +124,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
if (!string.IsNullOrEmpty(ProblemDetails.Detail))
{
writer.WriteElementString(
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Detail)),
XmlConvert.EncodeLocalName("detail"),
ProblemDetails.Detail);
}
if (!string.IsNullOrEmpty(ProblemDetails.Instance))
{
writer.WriteElementString(
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Instance)),
XmlConvert.EncodeLocalName("instance"),
ProblemDetails.Instance);
}
if (ProblemDetails.Status.HasValue)
{
writer.WriteStartElement(XmlConvert.EncodeLocalName(nameof(ProblemDetails.Status)));
writer.WriteStartElement(XmlConvert.EncodeLocalName("status"));
writer.WriteValue(ProblemDetails.Status.Value);
writer.WriteEndElement();
}
@ -143,14 +145,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
if (!string.IsNullOrEmpty(ProblemDetails.Title))
{
writer.WriteElementString(
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Title)),
XmlConvert.EncodeLocalName("title"),
ProblemDetails.Title);
}
if (!string.IsNullOrEmpty(ProblemDetails.Type))
{
writer.WriteElementString(
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Type)),
XmlConvert.EncodeLocalName("type"),
ProblemDetails.Type);
}

View File

@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
/// <summary>
/// Wrapper class for <see cref="ValidationProblemDetails"/> to enable it to be serialized by the xml formatters.
/// </summary>
[XmlRoot(nameof(ValidationProblemDetails))]
[XmlRoot("problem", Namespace = "urn:ietf:rfc:7807")]
public class ValidationProblemDetailsWrapper : ProblemDetailsWrapper, IUnwrappable
{
private static readonly string ErrorKey = "MVC-Errors";

View File

@ -17,14 +17,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
// Arrange
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ProblemDetails>" +
"<Title>Some title</Title>" +
"<Status>403</Status>" +
"<Instance>Some instance</Instance>" +
"<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<title>Some title</title>" +
"<status>403</status>" +
"<instance>Some instance</instance>" +
"<key1>Test Value 1</key1>" +
"<_x005B_key2_x005D_>Test Value 2</_x005B_key2_x005D_>" +
"<MVC-Empty>Test Value 3</MVC-Empty>" +
"</ProblemDetails>";
"</problem>";
var serializer = new DataContractSerializer(typeof(ProblemDetailsWrapper));
// Act
@ -76,13 +76,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var wrapper = new ProblemDetailsWrapper(problemDetails);
var outputStream = new MemoryStream();
var expectedContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ProblemDetails>" +
"<Detail>Some detail</Detail>" +
"<Title>Some title</Title>" +
"<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<detail>Some detail</detail>" +
"<title>Some title</title>" +
"<key1>Test Value 1</key1>" +
"<_x005B_Key2_x005D_>Test Value 2</_x005B_Key2_x005D_>" +
"<MVC-Empty>Test Value 3</MVC-Empty>" +
"</ProblemDetails>";
"</problem>";
// Act
using (var xmlWriter = XmlWriter.Create(outputStream))

View File

@ -17,10 +17,10 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
// Arrange
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Title>Some title</Title>" +
"<Status>400</Status>" +
"<Instance>Some instance</Instance>" +
"<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<title>Some title</title>" +
"<status>400</status>" +
"<instance>Some instance</instance>" +
"<key1>Test Value 1</key1>" +
"<_x005B_key2_x005D_>Test Value 2</_x005B_key2_x005D_>" +
"<MVC-Errors>" +
@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
"<_x005B_error2_x005D_>Test error 3</_x005B_error2_x005D_>" +
"<MVC-Empty>Test error 4</MVC-Empty>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
"</problem>";
var serializer = new DataContractSerializer(typeof(ValidationProblemDetailsWrapper));
// Act
@ -78,13 +78,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
// Arrange
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Title>Some title</Title>" +
"<Status>400</Status>" +
"<Instance>Some instance</Instance>" +
"<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<title>Some title</title>" +
"<status>400</status>" +
"<instance>Some instance</instance>" +
"<key1>Test Value 1</key1>" +
"<_x005B_key2_x005D_>Test Value 2</_x005B_key2_x005D_>" +
"</ValidationProblemDetails>";
"</problem>";
var serializer = new DataContractSerializer(typeof(ValidationProblemDetailsWrapper));
// Act
@ -118,11 +118,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
// Arrange
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Title>Some title</Title>" +
"<Status>400</Status>" +
"<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<title>Some title</title>" +
"<status>400</status>" +
"<MVC-Errors />" +
"</ValidationProblemDetails>";
"</problem>";
var serializer = new DataContractSerializer(typeof(ValidationProblemDetailsWrapper));
// Act
@ -160,9 +160,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var wrapper = new ValidationProblemDetailsWrapper(problemDetails);
var outputStream = new MemoryStream();
var expectedContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Detail>Some detail</Detail>" +
"<Title>Some title</Title>" +
"<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<detail>Some detail</detail>" +
"<title>Some title</title>" +
"<key1>Test Value 1</key1>" +
"<_x005B_Key2_x005D_>Test Value 2</_x005B_Key2_x005D_>" +
"<MVC-Errors>" +
@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
"<_x005B_error2_x005D_>Test error 3</_x005B_error2_x005D_>" +
"<MVC-Empty>Test error 4</MVC-Empty>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
"</problem>";
// Act
using (var xmlWriter = XmlWriter.Create(outputStream))
@ -203,12 +203,12 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
var wrapper = new ValidationProblemDetailsWrapper(problemDetails);
var outputStream = new MemoryStream();
var expectedContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Detail>Some detail</Detail>" +
"<Title>Some title</Title>" +
"<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<detail>Some detail</detail>" +
"<title>Some title</title>" +
"<key1>Test Value 1</key1>" +
"<_x005B_Key2_x005D_>Test Value 2</_x005B_Key2_x005D_>" +
"</ValidationProblemDetails>";
"</problem>";
// Act
using (var xmlWriter = XmlWriter.Create(outputStream))

View File

@ -216,12 +216,12 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Arrange
using (new ActivityReplacer())
{
var expected = "<ProblemDetails>" +
"<Status>404</Status>" +
"<Title>Not Found</Title>" +
"<Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type>" +
var expected = "<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<status>404</status>" +
"<title>Not Found</title>" +
"<type>https://tools.ietf.org/html/rfc7231#section-6.5.4</type>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"</ProblemDetails>";
"</problem>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningClientErrorStatusCodeResult");
@ -237,8 +237,13 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ProblemDetails_WithExtensionMembers_IsSerialized()
{
// Arrange
var expected = @"<ProblemDetails><Instance>instance</Instance><Status>404</Status><Title>title</Title>
<Correlation>correlation</Correlation><Accounts>Account1 Account2</Accounts></ProblemDetails>";
var expected = "<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<instance>instance</instance>" +
"<status>404</status>" +
"<title>title</title>" +
"<Correlation>correlation</Correlation>" +
"<Accounts>Account1 Account2</Accounts>" +
"</problem>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningProblemDetails");
@ -255,14 +260,14 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Arrange
using (new ActivityReplacer())
{
var expected = "<ValidationProblemDetails>" +
"<Status>400</Status>" +
"<Title>One or more validation errors occurred.</Title>" +
var expected = "<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<status>400</status>" +
"<title>One or more validation errors occurred.</title>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"<MVC-Errors>" +
"<State>The State field is required.</State>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
"</problem>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningValidationProblem");
@ -278,8 +283,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ValidationProblemDetails_WithExtensionMembers_IsSerialized()
{
// Arrange
var expected = @"<ValidationProblemDetails><Detail>some detail</Detail><Status>400</Status><Title>One or more validation errors occurred.</Title>
<Type>some type</Type><CorrelationId>correlation</CorrelationId><MVC-Errors><Error1>ErrorValue</Error1></MVC-Errors></ValidationProblemDetails>";
var expected = "<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<detail>some detail</detail>" +
"<status>400</status>" +
"<title>One or more validation errors occurred.</title>" +
"<type>some type</type>" +
"<CorrelationId>correlation</CorrelationId>" +
"<MVC-Errors>" +
"<Error1>ErrorValue</Error1>" +
"</MVC-Errors>" +
"</problem>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningValidationDetailsWithMetadata");

View File

@ -191,12 +191,12 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Arrange
using (new ActivityReplacer())
{
var expected = "<ProblemDetails>" +
"<Status>404</Status>" +
"<Title>Not Found</Title>" +
"<Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type>" +
var expected = "<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<status>404</status>" +
"<title>Not Found</title>" +
"<type>https://tools.ietf.org/html/rfc7231#section-6.5.4</type>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"</ProblemDetails>";
"</problem>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningClientErrorStatusCodeResult");
@ -212,8 +212,13 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ProblemDetails_WithExtensionMembers_IsSerialized()
{
// Arrange
var expected = @"<ProblemDetails><Instance>instance</Instance><Status>404</Status><Title>title</Title>
<Correlation>correlation</Correlation><Accounts>Account1 Account2</Accounts></ProblemDetails>";
var expected = "<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<instance>instance</instance>" +
"<status>404</status>" +
"<title>title</title>" +
"<Correlation>correlation</Correlation>" +
"<Accounts>Account1 Account2</Accounts>" +
"</problem>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningProblemDetails");
@ -230,14 +235,14 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Arrange
using (new ActivityReplacer())
{
var expected = "<ValidationProblemDetails>" +
"<Status>400</Status>" +
"<Title>One or more validation errors occurred.</Title>" +
var expected = "<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<status>400</status>" +
"<title>One or more validation errors occurred.</title>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"<MVC-Errors>" +
"<State>The State field is required.</State>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
"</problem>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningValidationProblem");
@ -253,8 +258,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ValidationProblemDetails_WithExtensionMembers_IsSerialized()
{
// Arrange
var expected = @"<ValidationProblemDetails><Detail>some detail</Detail><Status>400</Status><Title>One or more validation errors occurred.</Title>
<Type>some type</Type><CorrelationId>correlation</CorrelationId><MVC-Errors><Error1>ErrorValue</Error1></MVC-Errors></ValidationProblemDetails>";
var expected = "<problem xmlns=\"urn:ietf:rfc:7807\">" +
"<detail>some detail</detail>" +
"<status>400</status>" +
"<title>One or more validation errors occurred.</title>" +
"<type>some type</type>" +
"<CorrelationId>correlation</CorrelationId>" +
"<MVC-Errors>" +
"<Error1>ErrorValue</Error1>" +
"</MVC-Errors>" +
"</problem>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningValidationDetailsWithMetadata");