Polish ProblemDetails
* Add ability to set extended members on ProblemDetails * Skip empty valued properties when serializing ProblemDetails Fixes #8296 Fixes #8317
This commit is contained in:
parent
667ad4daff
commit
d09c3c9e28
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
|
|
@ -17,28 +18,47 @@ 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)]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence
|
||||
/// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence
|
||||
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
|
||||
/// see[RFC7231], Section 3.4).
|
||||
/// </summary>
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
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)]
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable explanation specific to this occurrence of the problem.
|
||||
/// </summary>
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
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.
|
||||
/// 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)]
|
||||
public string Instance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members.
|
||||
/// <para>
|
||||
/// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as
|
||||
/// other members of a problem type.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters.
|
||||
/// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters.
|
||||
/// </remarks>
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, object> Extensions { get; } = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,187 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
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))]
|
||||
public class ProblemDetailsWrapper : IXmlSerializable, IUnwrappable
|
||||
{
|
||||
/// <summary>
|
||||
/// Key used to represent dictionary elements with empty keys
|
||||
/// </summary>
|
||||
protected static readonly string EmptyKey = SerializableErrorWrapper.EmptyKey;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ProblemDetailsWrapper"/>.
|
||||
/// </summary>
|
||||
public ProblemDetailsWrapper()
|
||||
: this(new ProblemDetails())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ProblemDetailsWrapper"/>.
|
||||
/// </summary>
|
||||
public ProblemDetailsWrapper(ProblemDetails problemDetails)
|
||||
{
|
||||
ProblemDetails = problemDetails;
|
||||
}
|
||||
|
||||
internal ProblemDetails ProblemDetails { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public XmlSchema GetSchema() => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void ReadXml(XmlReader reader)
|
||||
{
|
||||
if (reader == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(reader));
|
||||
}
|
||||
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
reader.Read();
|
||||
return;
|
||||
}
|
||||
|
||||
reader.ReadStartElement();
|
||||
while (reader.NodeType != XmlNodeType.EndElement)
|
||||
{
|
||||
var key = XmlConvert.DecodeName(reader.LocalName);
|
||||
ReadValue(reader, key);
|
||||
|
||||
reader.MoveToContent();
|
||||
}
|
||||
|
||||
reader.ReadEndElement();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the value for the specified <paramref name="name"/> from the <paramref name="reader"/>.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||
/// <param name="name">The name of the node.</param>
|
||||
protected virtual void ReadValue(XmlReader reader, string name)
|
||||
{
|
||||
if (reader == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(reader));
|
||||
}
|
||||
|
||||
var value = reader.ReadInnerXml();
|
||||
|
||||
switch (name)
|
||||
{
|
||||
case nameof(ProblemDetails.Detail):
|
||||
ProblemDetails.Detail = value;
|
||||
break;
|
||||
|
||||
case nameof(ProblemDetails.Instance):
|
||||
ProblemDetails.Instance = value;
|
||||
break;
|
||||
|
||||
case nameof(ProblemDetails.Status):
|
||||
ProblemDetails.Status = string.IsNullOrEmpty(value) ?
|
||||
(int?)null :
|
||||
int.Parse(value, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
|
||||
case nameof(ProblemDetails.Title):
|
||||
ProblemDetails.Title = value;
|
||||
break;
|
||||
|
||||
case nameof(ProblemDetails.Type):
|
||||
ProblemDetails.Type = value;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (string.Equals(name, EmptyKey, StringComparison.Ordinal))
|
||||
{
|
||||
name = string.Empty;
|
||||
}
|
||||
|
||||
ProblemDetails.Extensions.Add(name, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteXml(XmlWriter writer)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ProblemDetails.Detail))
|
||||
{
|
||||
writer.WriteElementString(
|
||||
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Detail)),
|
||||
ProblemDetails.Detail);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ProblemDetails.Instance))
|
||||
{
|
||||
writer.WriteElementString(
|
||||
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Instance)),
|
||||
ProblemDetails.Instance);
|
||||
}
|
||||
|
||||
if (ProblemDetails.Status.HasValue)
|
||||
{
|
||||
writer.WriteStartElement(XmlConvert.EncodeLocalName(nameof(ProblemDetails.Status)));
|
||||
writer.WriteValue(ProblemDetails.Status.Value);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ProblemDetails.Title))
|
||||
{
|
||||
writer.WriteElementString(
|
||||
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Title)),
|
||||
ProblemDetails.Title);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ProblemDetails.Type))
|
||||
{
|
||||
writer.WriteElementString(
|
||||
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Type)),
|
||||
ProblemDetails.Type);
|
||||
}
|
||||
|
||||
foreach (var keyValuePair in ProblemDetails.Extensions)
|
||||
{
|
||||
var key = keyValuePair.Key;
|
||||
var value = keyValuePair.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
key = EmptyKey;
|
||||
}
|
||||
|
||||
writer.WriteStartElement(XmlConvert.EncodeLocalName(key));
|
||||
if (value != null)
|
||||
{
|
||||
writer.WriteValue(value);
|
||||
}
|
||||
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
object IUnwrappable.Unwrap(Type declaredType)
|
||||
{
|
||||
if (declaredType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(declaredType));
|
||||
}
|
||||
|
||||
return ProblemDetails;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Formatters.Xml.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
|||
{
|
||||
// Element name used when ModelStateEntry's Key is empty. Dash in element name should avoid collisions with
|
||||
// other ModelState entries because the character is not legal in an expression name.
|
||||
private static readonly string EmptyKey = "MVC-Empty";
|
||||
internal static readonly string EmptyKey = "MVC-Empty";
|
||||
|
||||
// Note: XmlSerializer requires to have default constructor
|
||||
public SerializableErrorWrapper()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
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))]
|
||||
public class ValidationProblemDetailsWrapper : ProblemDetailsWrapper, IUnwrappable
|
||||
{
|
||||
private static readonly string ErrorKey = "MVC-Errors";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ValidationProblemDetailsWrapper"/>.
|
||||
/// </summary>
|
||||
public ValidationProblemDetailsWrapper()
|
||||
: this(new ValidationProblemDetails())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ValidationProblemDetailsWrapper"/> for the specified
|
||||
/// <paramref name="problemDetails"/>.
|
||||
/// </summary>
|
||||
/// <param name="problemDetails">The <see cref="ProblemDetails"/>.</param>
|
||||
public ValidationProblemDetailsWrapper(ValidationProblemDetails problemDetails)
|
||||
: base(problemDetails)
|
||||
{
|
||||
ProblemDetails = problemDetails;
|
||||
}
|
||||
|
||||
internal new ValidationProblemDetails ProblemDetails { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ReadValue(XmlReader reader, string name)
|
||||
{
|
||||
if (reader == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(reader));
|
||||
}
|
||||
|
||||
if (string.Equals(name, ErrorKey, StringComparison.Ordinal))
|
||||
{
|
||||
reader.Read();
|
||||
ReadErrorProperty(reader);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.ReadValue(reader, name);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadErrorProperty(XmlReader reader)
|
||||
{
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (reader.NodeType != XmlNodeType.EndElement)
|
||||
{
|
||||
var key = XmlConvert.DecodeName(reader.LocalName);
|
||||
var value = reader.ReadInnerXml();
|
||||
if (string.Equals(EmptyKey, key, StringComparison.Ordinal))
|
||||
{
|
||||
key = string.Empty;
|
||||
}
|
||||
|
||||
ProblemDetails.Errors.Add(key, new[] { value });
|
||||
reader.MoveToContent();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteXml(XmlWriter writer)
|
||||
{
|
||||
if (writer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
|
||||
base.WriteXml(writer);
|
||||
|
||||
if (ProblemDetails.Errors.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartElement(XmlConvert.EncodeLocalName(ErrorKey));
|
||||
|
||||
foreach (var keyValuePair in ProblemDetails.Errors)
|
||||
{
|
||||
var key = keyValuePair.Key;
|
||||
var value = keyValuePair.Value;
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
key = EmptyKey;
|
||||
}
|
||||
|
||||
writer.WriteStartElement(XmlConvert.EncodeLocalName(key));
|
||||
if (value != null)
|
||||
{
|
||||
writer.WriteValue(value);
|
||||
}
|
||||
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
object IUnwrappable.Unwrap(Type declaredType)
|
||||
{
|
||||
if (declaredType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(declaredType));
|
||||
}
|
||||
|
||||
return ProblemDetails;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,5 +44,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static IList<IWrapperProviderFactory> GetDefaultProviderFactories()
|
||||
{
|
||||
var wrapperProviderFactories = new List<IWrapperProviderFactory>();
|
||||
|
||||
wrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
|
||||
|
||||
wrapperProviderFactories.Add(new WrapperProviderFactory(
|
||||
typeof(ProblemDetails),
|
||||
typeof(ProblemDetailsWrapper),
|
||||
value => new ProblemDetailsWrapper((ProblemDetails)value)));
|
||||
|
||||
wrapperProviderFactories.Add(new WrapperProviderFactory(
|
||||
typeof(ValidationProblemDetails),
|
||||
typeof(ValidationProblemDetailsWrapper),
|
||||
value => new ValidationProblemDetailsWrapper((ValidationProblemDetails)value)));
|
||||
|
||||
return wrapperProviderFactories;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
||||
{
|
||||
internal class WrapperProviderFactory : IWrapperProviderFactory
|
||||
{
|
||||
public WrapperProviderFactory(Type declaredType, Type wrappingType, Func<object, object> wrapper)
|
||||
{
|
||||
DeclaredType = declaredType;
|
||||
WrappingType = wrappingType;
|
||||
Wrapper = wrapper;
|
||||
}
|
||||
|
||||
public Type DeclaredType { get; }
|
||||
|
||||
public Type WrappingType { get; }
|
||||
|
||||
public Func<object, object> Wrapper { get; }
|
||||
|
||||
public IWrapperProvider GetProvider(WrapperProviderContext context)
|
||||
{
|
||||
if (context.DeclaredType == DeclaredType)
|
||||
{
|
||||
return new WrapperProvider(this);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private class WrapperProvider : IWrapperProvider
|
||||
{
|
||||
private readonly WrapperProviderFactory _wrapperFactory;
|
||||
|
||||
public WrapperProvider(WrapperProviderFactory wrapperFactory)
|
||||
{
|
||||
_wrapperFactory = wrapperFactory;
|
||||
}
|
||||
|
||||
public Type WrappingType => _wrapperFactory.WrappingType;
|
||||
|
||||
public object Wrap(object original)
|
||||
{
|
||||
return _wrapperFactory.Wrapper(original);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,8 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
_serializerSettings = new DataContractSerializerSettings();
|
||||
|
||||
WrapperProviderFactories = new List<IWrapperProviderFactory>();
|
||||
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
|
||||
WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -76,9 +76,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
_serializerSettings = new DataContractSerializerSettings();
|
||||
|
||||
WrapperProviderFactories = new List<IWrapperProviderFactory>();
|
||||
WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
|
||||
WrapperProviderFactories.Add(new EnumerableWrapperProviderFactory(WrapperProviderFactories));
|
||||
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
|
||||
|
||||
_logger = loggerFactory?.CreateLogger(GetType());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
|
||||
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
|
||||
|
||||
WrapperProviderFactories = new List<IWrapperProviderFactory>();
|
||||
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
|
||||
WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -73,9 +73,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
WriterSettings = writerSettings;
|
||||
|
||||
WrapperProviderFactories = new List<IWrapperProviderFactory>();
|
||||
WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
|
||||
WrapperProviderFactories.Add(new EnumerableWrapperProviderFactory(WrapperProviderFactories));
|
||||
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
|
||||
|
||||
_logger = loggerFactory?.CreateLogger(GetType());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
||||
{
|
||||
public class ProblemDetailsWrapperTest
|
||||
{
|
||||
[Fact]
|
||||
public void ReadXml_ReadsProblemDetailsXml()
|
||||
{
|
||||
// Arrange
|
||||
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
|
||||
"<ProblemDetails>" +
|
||||
"<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>";
|
||||
var serializer = new DataContractSerializer(typeof(ProblemDetailsWrapper));
|
||||
|
||||
// Act
|
||||
var value = serializer.ReadObject(
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(xml)));
|
||||
|
||||
// Assert
|
||||
var problemDetails = Assert.IsType<ProblemDetailsWrapper>(value).ProblemDetails;
|
||||
Assert.Equal("Some title", problemDetails.Title);
|
||||
Assert.Equal("Some instance", problemDetails.Instance);
|
||||
Assert.Equal(403, problemDetails.Status);
|
||||
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
Assert.Empty(kvp.Key);
|
||||
Assert.Equal("Test Value 3", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("[key2]", kvp.Key);
|
||||
Assert.Equal("Test Value 2", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("key1", kvp.Key);
|
||||
Assert.Equal("Test Value 1", kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void WriteXml_WritesValidXml()
|
||||
{
|
||||
// Arrange
|
||||
var problemDetails = new ProblemDetails
|
||||
{
|
||||
Title = "Some title",
|
||||
Detail = "Some detail",
|
||||
Extensions =
|
||||
{
|
||||
["key1"] = "Test Value 1",
|
||||
["[Key2]"] = "Test Value 2",
|
||||
[""] = "Test Value 3",
|
||||
},
|
||||
};
|
||||
|
||||
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>" +
|
||||
"<key1>Test Value 1</key1>" +
|
||||
"<_x005B_Key2_x005D_>Test Value 2</_x005B_Key2_x005D_>" +
|
||||
"<MVC-Empty>Test Value 3</MVC-Empty>" +
|
||||
"</ProblemDetails>";
|
||||
|
||||
// Act
|
||||
using (var xmlWriter = XmlWriter.Create(outputStream))
|
||||
{
|
||||
var dataContractSerializer = new DataContractSerializer(wrapper.GetType());
|
||||
dataContractSerializer.WriteObject(xmlWriter, wrapper);
|
||||
}
|
||||
outputStream.Position = 0;
|
||||
var res = new StreamReader(outputStream, Encoding.UTF8).ReadToEnd();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedContent, res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
||||
{
|
||||
public class ValidationProblemDetailsWrapperTest
|
||||
{
|
||||
[Fact]
|
||||
public void ReadXml_ReadsValidationProblemDetailsXml()
|
||||
{
|
||||
// Arrange
|
||||
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
|
||||
"<ValidationProblemDetails>" +
|
||||
"<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>" +
|
||||
"<error1>Test error 1 Test error 2</error1>" +
|
||||
"<_x005B_error2_x005D_>Test error 3</_x005B_error2_x005D_>" +
|
||||
"<MVC-Empty>Test error 4</MVC-Empty>" +
|
||||
"</MVC-Errors>" +
|
||||
"</ValidationProblemDetails>";
|
||||
var serializer = new DataContractSerializer(typeof(ValidationProblemDetailsWrapper));
|
||||
|
||||
// Act
|
||||
var value = serializer.ReadObject(
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(xml)));
|
||||
|
||||
// Assert
|
||||
var problemDetails = Assert.IsType<ValidationProblemDetailsWrapper>(value).ProblemDetails;
|
||||
Assert.Equal("Some title", problemDetails.Title);
|
||||
Assert.Equal("Some instance", problemDetails.Instance);
|
||||
Assert.Equal(400, problemDetails.Status);
|
||||
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("[key2]", kvp.Key);
|
||||
Assert.Equal("Test Value 2", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("key1", kvp.Key);
|
||||
Assert.Equal("Test Value 1", kvp.Value);
|
||||
});
|
||||
|
||||
Assert.Collection(
|
||||
problemDetails.Errors.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
Assert.Empty(kvp.Key);
|
||||
Assert.Equal(new[] { "Test error 4" }, kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("[error2]", kvp.Key);
|
||||
Assert.Equal(new[] { "Test error 3" }, kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("error1", kvp.Key);
|
||||
Assert.Equal(new[] { "Test error 1 Test error 2" }, kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadXml_ReadsValidationProblemDetailsXml_WithNoErrors()
|
||||
{
|
||||
// Arrange
|
||||
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
|
||||
"<ValidationProblemDetails>" +
|
||||
"<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>";
|
||||
var serializer = new DataContractSerializer(typeof(ValidationProblemDetailsWrapper));
|
||||
|
||||
// Act
|
||||
var value = serializer.ReadObject(
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(xml)));
|
||||
|
||||
// Assert
|
||||
var problemDetails = Assert.IsType<ValidationProblemDetailsWrapper>(value).ProblemDetails;
|
||||
Assert.Equal("Some title", problemDetails.Title);
|
||||
Assert.Equal("Some instance", problemDetails.Instance);
|
||||
Assert.Equal(400, problemDetails.Status);
|
||||
|
||||
Assert.Collection(
|
||||
problemDetails.Extensions,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("key1", kvp.Key);
|
||||
Assert.Equal("Test Value 1", kvp.Value);
|
||||
},
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("[key2]", kvp.Key);
|
||||
Assert.Equal("Test Value 2", kvp.Value);
|
||||
});
|
||||
|
||||
Assert.Empty(problemDetails.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadXml_ReadsValidationProblemDetailsXml_WithEmptyErrorsElement()
|
||||
{
|
||||
// Arrange
|
||||
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
|
||||
"<ValidationProblemDetails>" +
|
||||
"<Title>Some title</Title>" +
|
||||
"<Status>400</Status>" +
|
||||
"<MVC-Errors />" +
|
||||
"</ValidationProblemDetails>";
|
||||
var serializer = new DataContractSerializer(typeof(ValidationProblemDetailsWrapper));
|
||||
|
||||
// Act
|
||||
var value = serializer.ReadObject(
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(xml)));
|
||||
|
||||
// Assert
|
||||
var problemDetails = Assert.IsType<ValidationProblemDetailsWrapper>(value).ProblemDetails;
|
||||
Assert.Equal("Some title", problemDetails.Title);
|
||||
Assert.Equal(400, problemDetails.Status);
|
||||
Assert.Empty(problemDetails.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteXml_WritesValidXml()
|
||||
{
|
||||
// Arrange
|
||||
var problemDetails = new ValidationProblemDetails
|
||||
{
|
||||
Title = "Some title",
|
||||
Detail = "Some detail",
|
||||
Extensions =
|
||||
{
|
||||
["key1"] = "Test Value 1",
|
||||
["[Key2]"] = "Test Value 2"
|
||||
},
|
||||
Errors =
|
||||
{
|
||||
{ "error1", new[] {"Test error 1", "Test error 2" } },
|
||||
{ "[error2]", new[] {"Test error 3" } },
|
||||
{ "", new[] { "Test error 4" } },
|
||||
}
|
||||
};
|
||||
|
||||
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>" +
|
||||
"<key1>Test Value 1</key1>" +
|
||||
"<_x005B_Key2_x005D_>Test Value 2</_x005B_Key2_x005D_>" +
|
||||
"<MVC-Errors>" +
|
||||
"<error1>Test error 1 Test error 2</error1>" +
|
||||
"<_x005B_error2_x005D_>Test error 3</_x005B_error2_x005D_>" +
|
||||
"<MVC-Empty>Test error 4</MVC-Empty>" +
|
||||
"</MVC-Errors>" +
|
||||
"</ValidationProblemDetails>";
|
||||
|
||||
// Act
|
||||
using (var xmlWriter = XmlWriter.Create(outputStream))
|
||||
{
|
||||
var dataContractSerializer = new DataContractSerializer(wrapper.GetType());
|
||||
dataContractSerializer.WriteObject(xmlWriter, wrapper);
|
||||
}
|
||||
outputStream.Position = 0;
|
||||
var res = new StreamReader(outputStream, Encoding.UTF8).ReadToEnd();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedContent, res);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteXml_WithNoValidationErrors()
|
||||
{
|
||||
// Arrange
|
||||
var problemDetails = new ValidationProblemDetails
|
||||
{
|
||||
Title = "Some title",
|
||||
Detail = "Some detail",
|
||||
Extensions =
|
||||
{
|
||||
["key1"] = "Test Value 1",
|
||||
["[Key2]"] = "Test Value 2"
|
||||
},
|
||||
};
|
||||
|
||||
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>" +
|
||||
"<key1>Test Value 1</key1>" +
|
||||
"<_x005B_Key2_x005D_>Test Value 2</_x005B_Key2_x005D_>" +
|
||||
"</ValidationProblemDetails>";
|
||||
|
||||
// Act
|
||||
using (var xmlWriter = XmlWriter.Create(outputStream))
|
||||
{
|
||||
var dataContractSerializer = new DataContractSerializer(wrapper.GetType());
|
||||
dataContractSerializer.WriteObject(xmlWriter, wrapper);
|
||||
}
|
||||
outputStream.Position = 0;
|
||||
var res = new StreamReader(outputStream, Encoding.UTF8).ReadToEnd();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedContent, res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
||||
{
|
||||
public class WrapperProviderFactoryExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetDefaultProviderFactories_GetsFactoriesUsedByInputAndOutputFormatters()
|
||||
{
|
||||
// Act
|
||||
var factoryProviders = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
factoryProviders,
|
||||
factory => Assert.IsType<SerializableErrorWrapperProviderFactory>(factory),
|
||||
factory =>
|
||||
{
|
||||
var wrapperProviderFactory = Assert.IsType<WrapperProviderFactory>(factory);
|
||||
Assert.Equal(typeof(ProblemDetails), wrapperProviderFactory.DeclaredType);
|
||||
Assert.Equal(typeof(ProblemDetailsWrapper), wrapperProviderFactory.WrappingType);
|
||||
},
|
||||
factory =>
|
||||
{
|
||||
var wrapperProviderFactory = Assert.IsType<WrapperProviderFactory>(factory);
|
||||
Assert.Equal(typeof(ValidationProblemDetails), wrapperProviderFactory.DeclaredType);
|
||||
Assert.Equal(typeof(ValidationProblemDetailsWrapper), wrapperProviderFactory.WrappingType);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
|
||||
{
|
||||
public class WrapperProviderFactoryTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetProvider_ReturnsNull_IfTypeDoesNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new WrapperProviderFactory(
|
||||
typeof(ProblemDetails),
|
||||
typeof(ProblemDetailsWrapper),
|
||||
_ => null);
|
||||
var context = new WrapperProviderContext(typeof(SerializableError), isSerialization: true);
|
||||
|
||||
// Act
|
||||
var result = provider.GetProvider(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProvider_ReturnsNull_IfTypeIsSubtype()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new WrapperProviderFactory(
|
||||
typeof(ProblemDetails),
|
||||
typeof(ProblemDetailsWrapper),
|
||||
_ => null);
|
||||
var context = new WrapperProviderContext(typeof(ValidationProblemDetails), isSerialization: true);
|
||||
|
||||
// Act
|
||||
var result = provider.GetProvider(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetProvider_ReturnsValue_IfTypeMatches()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new object();
|
||||
var providerFactory = new WrapperProviderFactory(
|
||||
typeof(ProblemDetails),
|
||||
typeof(ProblemDetailsWrapper),
|
||||
_ => expected);
|
||||
var context = new WrapperProviderContext(typeof(ProblemDetails), isSerialization: true);
|
||||
|
||||
// Act
|
||||
var provider = providerFactory.GetProvider(context);
|
||||
var result = provider.Wrap(new ProblemDetails());
|
||||
|
||||
// Assert
|
||||
Assert.Same(expected, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
|||
using BasicWebSite.Models;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
|
|
@ -116,8 +117,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
};
|
||||
var expected = new Dictionary<string, string[]>
|
||||
{
|
||||
{"Name", new string[] {"The field Name must be a string with a minimum length of 5 and a maximum length of 30."}},
|
||||
{"Zip", new string[]{ @"The field Zip must match the regular expression '\d{5}'."}}
|
||||
{"Name", new[] {"The field Name must be a string with a minimum length of 5 and a maximum length of 30."}},
|
||||
{"Zip", new[] { @"The field Zip must match the regular expression '\d{5}'."}}
|
||||
};
|
||||
var contactString = JsonConvert.SerializeObject(contactModel);
|
||||
|
||||
|
|
@ -261,5 +262,62 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(content);
|
||||
Assert.Equal(404, problemDetails.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SerializingProblemDetails_IgnoresNullValuedProperties()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new[] { "status", "title", "type" };
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/contact/ActionReturningStatusCodeResult");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Verify that null-valued properties on ProblemDetails are not serialized.
|
||||
var json = JObject.Parse(content);
|
||||
Assert.Equal(expected, json.Properties().OrderBy(p => p.Name).Select(p => p.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SerializingProblemDetails_WithAllValuesSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new[] { "detail", "instance", "status", "title", "tracking-id", "type" };
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/contact/ActionReturningProblemDetails");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var json = JObject.Parse(content);
|
||||
Assert.Equal(expected, json.Properties().OrderBy(p => p.Name).Select(p => p.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SerializingValidationProblemDetails_WithExtensionData()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetAsync("/contact/ActionReturningValidationProblemDetails");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
var validationProblemDetails = JsonConvert.DeserializeObject<ValidationProblemDetails>(content);
|
||||
|
||||
Assert.Equal("Error", validationProblemDetails.Title);
|
||||
Assert.Equal(400, validationProblemDetails.Status);
|
||||
Assert.Equal("27", validationProblemDetails.Extensions["tracking-id"]);
|
||||
Assert.Collection(
|
||||
validationProblemDetails.Errors,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("Error1", kvp.Key);
|
||||
Assert.Equal(new[] { "Error Message" }, kvp.Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
throw new StatusCodeMismatchException
|
||||
{
|
||||
ExpectedStatusCode = HttpStatusCode.OK,
|
||||
ExpectedStatusCode = expectedStatusCode,
|
||||
ActualStatusCode = response.StatusCode,
|
||||
ResponseContent = responseContent,
|
||||
};
|
||||
|
|
@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
{
|
||||
get
|
||||
{
|
||||
return $"Excepted status code 200. Actual {ActualStatusCode}. Response Content:" + Environment.NewLine + ResponseContent;
|
||||
return $"Excepted status code {ExpectedStatusCode}. Actual {ActualStatusCode}. Response Content:" + Environment.NewLine + ResponseContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,5 +208,68 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
"</ArrayOfSerializableErrorWrapper>",
|
||||
result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProblemDetails_IsSerialized()
|
||||
{
|
||||
// Arrange
|
||||
var expected = @"<ProblemDetails><Status>404</Status><Title>Not Found</Title><Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type></ProblemDetails>";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningClientErrorStatusCodeResult");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
XmlAssert.Equal(expected, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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>";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningProblemDetails");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
XmlAssert.Equal(expected, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidationProblemDetails_IsSerialized()
|
||||
{
|
||||
// Arrange
|
||||
var expected = @"<ValidationProblemDetails><Status>400</Status><Title>One or more validation errors occurred.</Title>
|
||||
<MVC-Errors><State>The State field is required.</State></MVC-Errors></ValidationProblemDetails>";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningValidationProblem");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
XmlAssert.Equal(expected, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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>";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningValidationDetailsWithMetadata");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
XmlAssert.Equal(expected, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,5 +183,68 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
"<key4>key2-error</key4></SerializableErrorWrapper></ArrayOfSerializableErrorWrapper>",
|
||||
result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProblemDetails_IsSerialized()
|
||||
{
|
||||
// Arrange
|
||||
var expected = @"<ProblemDetails><Status>404</Status><Title>Not Found</Title><Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type></ProblemDetails>";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningClientErrorStatusCodeResult");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
XmlAssert.Equal(expected, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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>";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningProblemDetails");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
XmlAssert.Equal(expected, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidationProblemDetails_IsSerialized()
|
||||
{
|
||||
// Arrange
|
||||
var expected = @"<ValidationProblemDetails><Status>400</Status><Title>One or more validation errors occurred.</Title>
|
||||
<MVC-Errors><State>The State field is required.</State></MVC-Errors></ValidationProblemDetails>";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningValidationProblem");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
XmlAssert.Equal(expected, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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>";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningValidationDetailsWithMetadata");
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
XmlAssert.Equal(expected, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,41 @@ namespace BasicWebSite
|
|||
return NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("[action]")]
|
||||
public ActionResult<int> ActionReturningProblemDetails()
|
||||
{
|
||||
return NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Not Found",
|
||||
Type = "Type",
|
||||
Detail = "Detail",
|
||||
Status = 404,
|
||||
Instance = "Instance",
|
||||
Extensions =
|
||||
{
|
||||
["tracking-id"] = 27,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("[action]")]
|
||||
public ActionResult<int> ActionReturningValidationProblemDetails()
|
||||
{
|
||||
return BadRequest(new ValidationProblemDetails
|
||||
{
|
||||
Title = "Error",
|
||||
Status = 400,
|
||||
Extensions =
|
||||
{
|
||||
["tracking-id"] = "27",
|
||||
},
|
||||
Errors =
|
||||
{
|
||||
{ "Error1", new[] { "Error Message" } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private class TestModelBinder : IModelBinder
|
||||
{
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using XmlFormattersWebSite.Models;
|
||||
|
||||
namespace XmlFormattersWebSite
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]/[action]")]
|
||||
public abstract class XmlApiControllerBase : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public ActionResult<Person> ActionReturningClientErrorStatusCodeResult()
|
||||
=> NotFound();
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<Person> ActionReturningProblemDetails()
|
||||
{
|
||||
return NotFound(new ProblemDetails
|
||||
{
|
||||
Instance = "instance",
|
||||
Title = "title",
|
||||
Extensions =
|
||||
{
|
||||
["Correlation"] = "correlation",
|
||||
["Accounts"] = new[] { "Account1", "Account2" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<Person> ActionReturningValidationProblem([FromQuery] Address address)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
[HttpGet]
|
||||
public ActionResult<Person> ActionReturningValidationDetailsWithMetadata()
|
||||
{
|
||||
return new BadRequestObjectResult(new ValidationProblemDetails
|
||||
{
|
||||
Detail = "some detail",
|
||||
Type = "some type",
|
||||
Extensions =
|
||||
{
|
||||
["CorrelationId"] = "correlation",
|
||||
},
|
||||
Errors =
|
||||
{
|
||||
["Error1"] = new[] { "ErrorValue"},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace XmlFormattersWebSite
|
||||
{
|
||||
[SetupOutputFormatters]
|
||||
public class XmlDataContractApiController : XmlApiControllerBase
|
||||
{
|
||||
private class SetupOutputFormattersAttribute : ResultFilterAttribute
|
||||
{
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
{
|
||||
if (!(context.Result is ObjectResult objectResult))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Both kinds of Xml serializers are configured for this application and use custom content-types to do formatter
|
||||
// selection. The globally configured formatters rely on custom content-type to perform conneg which does not play
|
||||
// well the ProblemDetails returning filters that defaults to using application/xml. We'll explicitly select the formatter for this controller.
|
||||
objectResult.Formatters.Add(new XmlDataContractSerializerOutputFormatter());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
||||
namespace XmlFormattersWebSite
|
||||
{
|
||||
[SetupOutputFormatters]
|
||||
public class XmlSerializerApiController : XmlApiControllerBase
|
||||
{
|
||||
private class SetupOutputFormattersAttribute : ResultFilterAttribute
|
||||
{
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
{
|
||||
if (!(context.Result is ObjectResult objectResult))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Both kinds of Xml serializers are configured for this application and use custom content-types to do formatter
|
||||
// selection. The globally configured formatters rely on custom content-type to perform conneg which does not play
|
||||
// well the ProblemDetails returning filters that defaults to using application/xml. We'll explicitly select the formatter for this controller.
|
||||
objectResult.Formatters.Add(new XmlSerializerOutputFormatter());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue