diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ProblemDetails.cs b/src/Microsoft.AspNetCore.Mvc.Core/ProblemDetails.cs
index 573419b0bc..44b816aa05 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ProblemDetails.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ProblemDetails.cs
@@ -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".
///
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Type { get; set; }
///
- /// 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).
///
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Title { get; set; }
///
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
///
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int? Status { get; set; }
///
/// A human-readable explanation specific to this occurrence of the problem.
///
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Detail { get; set; }
///
- /// 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.
///
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Instance { get; set; }
+
+ ///
+ /// Gets the for extension members.
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ /// The round-tripping behavior for 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.
+ ///
+ [JsonExtensionData]
+ public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal);
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetailsWrapper.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetailsWrapper.cs
new file mode 100644
index 0000000000..30775bd1b1
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetailsWrapper.cs
@@ -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
+{
+ ///
+ /// Wrapper class for to enable it to be serialized by the xml formatters.
+ ///
+ [XmlRoot(nameof(ProblemDetails))]
+ public class ProblemDetailsWrapper : IXmlSerializable, IUnwrappable
+ {
+ ///
+ /// Key used to represent dictionary elements with empty keys
+ ///
+ protected static readonly string EmptyKey = SerializableErrorWrapper.EmptyKey;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public ProblemDetailsWrapper()
+ : this(new ProblemDetails())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public ProblemDetailsWrapper(ProblemDetails problemDetails)
+ {
+ ProblemDetails = problemDetails;
+ }
+
+ internal ProblemDetails ProblemDetails { get; }
+
+ ///
+ public XmlSchema GetSchema() => null;
+
+ ///
+ 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();
+ }
+
+ ///
+ /// Reads the value for the specified from the .
+ ///
+ /// The .
+ /// The name of the node.
+ 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;
+ }
+ }
+
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..61797e230b
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Properties/AssemblyInfo.cs
@@ -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")]
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/SerializableErrorWrapper.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/SerializableErrorWrapper.cs
index e8a28f576f..8a72825a80 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/SerializableErrorWrapper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/SerializableErrorWrapper.cs
@@ -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()
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ValidationProblemDetailsWrapper.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ValidationProblemDetailsWrapper.cs
new file mode 100644
index 0000000000..b8787ee0e0
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ValidationProblemDetailsWrapper.cs
@@ -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
+{
+ ///
+ /// Wrapper class for to enable it to be serialized by the xml formatters.
+ ///
+ [XmlRoot(nameof(ValidationProblemDetails))]
+ public class ValidationProblemDetailsWrapper : ProblemDetailsWrapper, IUnwrappable
+ {
+ private static readonly string ErrorKey = "MVC-Errors";
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public ValidationProblemDetailsWrapper()
+ : this(new ValidationProblemDetails())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of for the specified
+ /// .
+ ///
+ /// The .
+ public ValidationProblemDetailsWrapper(ValidationProblemDetails problemDetails)
+ : base(problemDetails)
+ {
+ ProblemDetails = problemDetails;
+ }
+
+ internal new ValidationProblemDetails ProblemDetails { get; }
+
+ ///
+ 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();
+ }
+ }
+
+ ///
+ 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactoriesExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactoriesExtensions.cs
index 1ccea1645d..6ff62a8ec0 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactoriesExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactoriesExtensions.cs
@@ -44,5 +44,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
return null;
}
+
+ internal static IList GetDefaultProviderFactories()
+ {
+ var wrapperProviderFactories = new List();
+
+ 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;
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactory.cs
new file mode 100644
index 0000000000..3f7c4a48af
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactory.cs
@@ -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