From a40c1f2d026b96c791e4cf88432555b92da008cf Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 11 Oct 2018 10:46:29 -0700 Subject: [PATCH] Use compat flag to drive XML ProblemDetails formatting --- .../MvcXmlMvcBuilderExtensions.cs | 80 +++++- .../MvcXmlMvcCoreBuilderExtensions.cs | 81 ++++++- .../MvcXmlOptions.cs | 68 ++++++ ...XmlOptionsConfigureCompatibilityOptions.cs | 36 +++ .../ProblemDetails21Wrapper.cs | 179 ++++++++++++++ .../ProblemDetailsWrapperProviderFactory.cs | 65 +++++ .../ValidationProblemDetails21Wrapper.cs | 127 ++++++++++ .../WrapperProviderFactoriesExtensions.cs | 19 -- .../WrapperProviderFactory.cs | 50 ---- ...XmlDataContractSerializerInputFormatter.cs | 5 +- ...lDataContractSerializerMvcOptionsSetup.cs} | 33 ++- ...mlDataContractSerializerOutputFormatter.cs | 5 +- .../XmlSerializerInputFormatter.cs | 5 +- ...tup.cs => XmlSerializerMvcOptionsSetup.cs} | 32 ++- .../XmlSerializerOutputFormatter.cs | 5 +- ...taContractSerializerMvcOptionsSetupTest.cs | 42 ---- .../MvcXmlSerializerMvcOptionsSetupTest.cs | 42 ---- .../ProblemDetails21WrapperTest.cs | 102 ++++++++ ...roblemDetailsWrapperProviderFactoryTest.cs | 119 +++++++++ .../ProblemDetailsWrapperTest.cs | 1 - .../ValidationProblemDetails21WrapperTest.cs | 228 ++++++++++++++++++ .../WrapperProviderFactoryExtensionsTest.cs | 34 --- .../WrapperProviderFactoryTest.cs | 63 ----- ...taContractSerializerMvcOptionsSetupTest.cs | 71 ++++++ .../XmlSerializerMvcOptionsSetupTest.cs | 71 ++++++ ...ontractSerializerFormattersWrappingTest.cs | 61 ++++- .../XmlSerializerFormattersWrappingTest.cs | 61 ++++- .../TestMvcOptions.cs | 4 - .../CompatibilitySwitchIntegrationTest.cs | 29 ++- .../XmlDataContractApiController.cs | 8 +- .../Controllers/XmlSerializedApiController.cs | 8 +- test/WebSites/XmlFormattersWebSite/Startup.cs | 76 ++++-- .../StartupWith21Compat.cs | 13 + 33 files changed, 1505 insertions(+), 318 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Formatters.Xml/MvcXmlOptions.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Formatters.Xml/MvcXmlOptionsConfigureCompatibilityOptions.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetails21Wrapper.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetailsWrapperProviderFactory.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ValidationProblemDetails21Wrapper.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactory.cs rename src/Microsoft.AspNetCore.Mvc.Formatters.Xml/{Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs => XmlDataContractSerializerMvcOptionsSetup.cs} (57%) rename src/Microsoft.AspNetCore.Mvc.Formatters.Xml/{Internal/MvcXmlSerializerMvcOptionsSetup.cs => XmlSerializerMvcOptionsSetup.cs} (51%) delete mode 100644 test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlDataContractSerializerMvcOptionsSetupTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlSerializerMvcOptionsSetupTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetails21WrapperTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetailsWrapperProviderFactoryTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ValidationProblemDetails21WrapperTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/WrapperProviderFactoryExtensionsTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/WrapperProviderFactoryTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerMvcOptionsSetupTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlSerializerMvcOptionsSetupTest.cs create mode 100644 test/WebSites/XmlFormattersWebSite/StartupWith21Compat.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/DependencyInjection/MvcXmlMvcBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/DependencyInjection/MvcXmlMvcBuilderExtensions.cs index 4210fbe0bc..17bce01d5e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/DependencyInjection/MvcXmlMvcBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/DependencyInjection/MvcXmlMvcBuilderExtensions.cs @@ -3,7 +3,7 @@ using System; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; +using Microsoft.AspNetCore.Mvc.Formatters.Xml; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -14,6 +14,29 @@ namespace Microsoft.Extensions.DependencyInjection /// public static class MvcXmlMvcBuilderExtensions { + /// + /// Adds configuration of for the application. + /// + /// The . + /// The which need to be configured. + public static IMvcBuilder AddXmlOptions( + this IMvcBuilder builder, + Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + builder.Services.Configure(setupAction); + return builder; + } + /// /// Adds the XML DataContractSerializer formatters to MVC. /// @@ -30,6 +53,31 @@ namespace Microsoft.Extensions.DependencyInjection return builder; } + /// + /// Adds the XML DataContractSerializer formatters to MVC. + /// + /// The . + /// The which need to be configured. + /// The . + public static IMvcBuilder AddXmlDataContractSerializerFormatters( + this IMvcBuilder builder, + Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + AddXmlDataContractSerializerFormatterServices(builder.Services); + builder.Services.Configure(setupAction); + return builder; + } + /// /// Adds the XML Serializer formatters to MVC. /// @@ -46,18 +94,44 @@ namespace Microsoft.Extensions.DependencyInjection return builder; } + /// + /// Adds the XML Serializer formatters to MVC. + /// + /// The . + /// The which need to be configured. + /// The . + public static IMvcBuilder AddXmlSerializerFormatters( + this IMvcBuilder builder, + Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + AddXmlSerializerFormatterServices(builder.Services); + builder.Services.Configure(setupAction); + return builder; + } + // Internal for testing. internal static void AddXmlDataContractSerializerFormatterServices(IServiceCollection services) { services.TryAddEnumerable( - ServiceDescriptor.Transient, MvcXmlDataContractSerializerMvcOptionsSetup>()); + ServiceDescriptor.Transient, XmlDataContractSerializerMvcOptionsSetup>()); + + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcXmlOptionsConfigureCompatibilityOptions>()); } // Internal for testing. internal static void AddXmlSerializerFormatterServices(IServiceCollection services) { services.TryAddEnumerable( - ServiceDescriptor.Transient, MvcXmlSerializerMvcOptionsSetup>()); + ServiceDescriptor.Transient, XmlSerializerMvcOptionsSetup>()); + + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcXmlOptionsConfigureCompatibilityOptions>()); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/DependencyInjection/MvcXmlMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/DependencyInjection/MvcXmlMvcCoreBuilderExtensions.cs index 5e14f41647..9608d3fc18 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/DependencyInjection/MvcXmlMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/DependencyInjection/MvcXmlMvcCoreBuilderExtensions.cs @@ -3,7 +3,7 @@ using System; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; +using Microsoft.AspNetCore.Mvc.Formatters.Xml; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -14,6 +14,30 @@ namespace Microsoft.Extensions.DependencyInjection /// public static class MvcXmlMvcCoreBuilderExtensions { + /// + /// Adds configuration of for the application. + /// + /// The . + /// The which need to be configured. + /// The . + public static IMvcCoreBuilder AddXmlOptions( + this IMvcCoreBuilder builder, + Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + builder.Services.Configure(setupAction); + return builder; + } + /// /// Adds the XML DataContractSerializer formatters to MVC. /// @@ -30,6 +54,31 @@ namespace Microsoft.Extensions.DependencyInjection return builder; } + /// + /// Adds the XML DataContractSerializer formatters to MVC. + /// + /// The . + /// The which need to be configured. + /// The . + public static IMvcCoreBuilder AddXmlDataContractSerializerFormatters( + this IMvcCoreBuilder builder, + Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (setupAction == null) + { + throw new ArgumentNullException(nameof(setupAction)); + } + + AddXmlDataContractSerializerFormatterServices(builder.Services); + builder.Services.Configure(setupAction); + return builder; + } + /// /// Adds the XML Serializer formatters to MVC. /// @@ -46,18 +95,44 @@ namespace Microsoft.Extensions.DependencyInjection return builder; } + /// + /// Adds the XML Serializer formatters to MVC. + /// + /// The . + /// The which need to be configured. + /// /// The . + public static IMvcCoreBuilder AddXmlSerializerFormatters( + this IMvcCoreBuilder builder, + Action setupAction) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + AddXmlSerializerFormatterServices(builder.Services); + builder.Services.Configure(setupAction); + return builder; + } + // Internal for testing. internal static void AddXmlDataContractSerializerFormatterServices(IServiceCollection services) { services.TryAddEnumerable( - ServiceDescriptor.Transient, MvcXmlDataContractSerializerMvcOptionsSetup>()); + ServiceDescriptor.Transient, XmlDataContractSerializerMvcOptionsSetup>()); + + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcXmlOptionsConfigureCompatibilityOptions>()); } // Internal for testing. internal static void AddXmlSerializerFormatterServices(IServiceCollection services) { services.TryAddEnumerable( - ServiceDescriptor.Transient, MvcXmlSerializerMvcOptionsSetup>()); + ServiceDescriptor.Transient, XmlSerializerMvcOptionsSetup>()); + + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcXmlOptionsConfigureCompatibilityOptions>()); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/MvcXmlOptions.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/MvcXmlOptions.cs new file mode 100644 index 0000000000..d8ada74590 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/MvcXmlOptions.cs @@ -0,0 +1,68 @@ +// 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.Collections; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.Formatters.Xml +{ + /// + /// Provides configuration for XML formatters. + /// + public class MvcXmlOptions : IEnumerable + { + private readonly CompatibilitySwitch _allowRfc7807CompliantProblemDetailsFormat; + private readonly IReadOnlyList _switches; + + /// + /// Creates a new instance of . + /// + public MvcXmlOptions() + { + _allowRfc7807CompliantProblemDetailsFormat = new CompatibilitySwitch(nameof(AllowRfc7807CompliantProblemDetailsFormat)); + + _switches = new ICompatibilitySwitch[] + { + _allowRfc7807CompliantProblemDetailsFormat, + }; + } + + /// + /// Gets or sets a value inidicating whether and + /// are serialized in a format compliant with the RFC 7807 specification (https://tools.ietf.org/html/rfc7807). + /// + /// + /// The default value is if the version is + /// or later; otherwise. + /// + /// + /// + /// This property is associated with a compatibility switch and can provide a different behavior depending on + /// the configured compatibility version for the application. See for + /// guidance and examples of setting the application's compatibility version. + /// + /// + /// Configuring the desired value of the compatibility switch by calling this property's setter will take + /// precedence over the value implied by the application's . + /// + /// + /// If the application's compatibility version is set to or + /// lower then this setting will have the value unless explicitly configured. + /// + /// + /// If the application's compatibility version is set to or + /// higher then this setting will have the value unless explicitly configured. + /// + /// + public bool AllowRfc7807CompliantProblemDetailsFormat + { + get => _allowRfc7807CompliantProblemDetailsFormat.Value; + set => _allowRfc7807CompliantProblemDetailsFormat.Value = value; + } + + public IEnumerator GetEnumerator() => _switches.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/MvcXmlOptionsConfigureCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/MvcXmlOptionsConfigureCompatibilityOptions.cs new file mode 100644 index 0000000000..c5d1d3e340 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/MvcXmlOptionsConfigureCompatibilityOptions.cs @@ -0,0 +1,36 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Formatters.Xml; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc +{ + internal sealed class MvcXmlOptionsConfigureCompatibilityOptions : ConfigureCompatibilityOptions + { + public MvcXmlOptionsConfigureCompatibilityOptions( + ILoggerFactory loggerFactory, + IOptions compatibilityOptions) + : base(loggerFactory, compatibilityOptions) + { + } + + protected override IReadOnlyDictionary DefaultValues + { + get + { + var values = new Dictionary(); + + if (Version >= CompatibilityVersion.Version_2_2) + { + values[nameof(MvcXmlOptions.AllowRfc7807CompliantProblemDetailsFormat)] = true; + } + + return values; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetails21Wrapper.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetails21Wrapper.cs new file mode 100644 index 0000000000..9a4dcf0bd7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetails21Wrapper.cs @@ -0,0 +1,179 @@ +// 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))] + [Obsolete("This type is deprecated and will be removed in a future version")] + public class ProblemDetails21Wrapper : IXmlSerializable, IUnwrappable + { + protected static readonly string EmptyKey = SerializableErrorWrapper.EmptyKey; + + public ProblemDetails21Wrapper() + : this(new ProblemDetails()) + { + } + + public ProblemDetails21Wrapper(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 "Detail": + ProblemDetails.Detail = value; + break; + + case "Instance": + ProblemDetails.Instance = value; + break; + + case "Status": + ProblemDetails.Status = string.IsNullOrEmpty(value) ? + (int?)null : + int.Parse(value, CultureInfo.InvariantCulture); + break; + + case "Title": + ProblemDetails.Title = value; + break; + + case "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("Detail"), + ProblemDetails.Detail); + } + + if (!string.IsNullOrEmpty(ProblemDetails.Instance)) + { + writer.WriteElementString( + XmlConvert.EncodeLocalName("Instance"), + ProblemDetails.Instance); + } + + if (ProblemDetails.Status.HasValue) + { + writer.WriteStartElement(XmlConvert.EncodeLocalName("Status")); + writer.WriteValue(ProblemDetails.Status.Value); + writer.WriteEndElement(); + } + + if (!string.IsNullOrEmpty(ProblemDetails.Title)) + { + writer.WriteElementString( + XmlConvert.EncodeLocalName("Title"), + ProblemDetails.Title); + } + + if (!string.IsNullOrEmpty(ProblemDetails.Type)) + { + writer.WriteElementString( + XmlConvert.EncodeLocalName("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/ProblemDetailsWrapperProviderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetailsWrapperProviderFactory.cs new file mode 100644 index 0000000000..9b93b86c6b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ProblemDetailsWrapperProviderFactory.cs @@ -0,0 +1,65 @@ +// 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 ProblemDetailsWrapperProviderFactory : IWrapperProviderFactory + { + private readonly MvcXmlOptions _options; + + public ProblemDetailsWrapperProviderFactory(MvcXmlOptions options) + { + _options = options; + } + + public IWrapperProvider GetProvider(WrapperProviderContext context) + { + if (context.DeclaredType == typeof(ProblemDetails)) + { + if (_options.AllowRfc7807CompliantProblemDetailsFormat) + { + return new WrapperProvider(typeof(ProblemDetailsWrapper), p => new ProblemDetailsWrapper((ProblemDetails)p)); + } + else + { +#pragma warning disable CS0618 // Type or member is obsolete + return new WrapperProvider(typeof(ProblemDetails21Wrapper), p => new ProblemDetails21Wrapper((ProblemDetails)p)); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + if (context.DeclaredType == typeof(ValidationProblemDetails)) + { + if (_options.AllowRfc7807CompliantProblemDetailsFormat) + { + return new WrapperProvider(typeof(ValidationProblemDetailsWrapper), p => new ValidationProblemDetailsWrapper((ValidationProblemDetails)p)); + } + else + { +#pragma warning disable CS0618 // Type or member is obsolete + return new WrapperProvider(typeof(ValidationProblemDetails21Wrapper), p => new ValidationProblemDetails21Wrapper((ValidationProblemDetails)p)); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + return null; + } + + private class WrapperProvider : IWrapperProvider + { + public WrapperProvider(Type wrappingType, Func wrapDelegate) + { + WrappingType = wrappingType; + WrapDelegate = wrapDelegate; + } + + public Type WrappingType { get; } + + public Func WrapDelegate { get; } + + public object Wrap(object original) => WrapDelegate(original); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ValidationProblemDetails21Wrapper.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ValidationProblemDetails21Wrapper.cs new file mode 100644 index 0000000000..e138c0f3d1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/ValidationProblemDetails21Wrapper.cs @@ -0,0 +1,127 @@ +// 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))] + [Obsolete("This type is deprecated and will be removed in a future version")] + public class ValidationProblemDetails21Wrapper : ProblemDetails21Wrapper, IUnwrappable + { + private static readonly string ErrorKey = "MVC-Errors"; + + /// + /// Initializes a new instance of . + /// + public ValidationProblemDetails21Wrapper() + : this(new ValidationProblemDetails()) + { + } + + /// + /// Initializes a new instance of for the specified + /// . + /// + /// The . + public ValidationProblemDetails21Wrapper(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 6ff62a8ec0..1ccea1645d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactoriesExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactoriesExtensions.cs @@ -44,24 +44,5 @@ 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 deleted file mode 100644 index 3f7c4a48af..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/WrapperProviderFactory.cs +++ /dev/null @@ -1,50 +0,0 @@ -// 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 wrapper) - { - DeclaredType = declaredType; - WrappingType = wrappingType; - Wrapper = wrapper; - } - - public Type DeclaredType { get; } - - public Type WrappingType { get; } - - public Func 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); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs index b5d6d74a22..f0954c36a4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs @@ -46,7 +46,10 @@ namespace Microsoft.AspNetCore.Mvc.Formatters _serializerSettings = new DataContractSerializerSettings(); - WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories(); + WrapperProviderFactories = new List + { + new SerializableErrorWrapperProviderFactory(), + }; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerMvcOptionsSetup.cs similarity index 57% rename from src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs rename to src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerMvcOptionsSetup.cs index c1cd77f685..eba6b9d5fd 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerMvcOptionsSetup.cs @@ -2,34 +2,36 @@ // 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.Linq; +using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal +namespace Microsoft.AspNetCore.Mvc.Formatters.Xml { /// /// A implementation which will add the /// data contract serializer formatters to . /// - public class MvcXmlDataContractSerializerMvcOptionsSetup : IConfigureOptions + internal sealed class XmlDataContractSerializerMvcOptionsSetup : IConfigureOptions { + private readonly MvcXmlOptions _xmlOptions; private readonly ILoggerFactory _loggerFactory; /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// + /// . /// The . - public MvcXmlDataContractSerializerMvcOptionsSetup(ILoggerFactory loggerFactory) + public XmlDataContractSerializerMvcOptionsSetup( + IOptions xmlOptions, + ILoggerFactory loggerFactory) { - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - _loggerFactory = loggerFactory; + _xmlOptions = xmlOptions?.Value ?? throw new ArgumentNullException(nameof(xmlOptions)); + _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } /// @@ -40,8 +42,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal { options.ModelMetadataDetailsProviders.Add(new DataMemberRequiredBindingMetadataProvider()); - options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter(_loggerFactory)); - options.InputFormatters.Add(new XmlDataContractSerializerInputFormatter(options)); + var inputFormatter = new XmlDataContractSerializerInputFormatter(options); + inputFormatter.WrapperProviderFactories.Add(new ProblemDetailsWrapperProviderFactory(_xmlOptions)); + options.InputFormatters.Add(inputFormatter); + + var outputFormatter = new XmlDataContractSerializerOutputFormatter(_loggerFactory); + outputFormatter.WrapperProviderFactories.Add(new ProblemDetailsWrapperProviderFactory(_xmlOptions)); + options.OutputFormatters.Add(outputFormatter); // Do not override any user mapping var key = "xml"; diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs index 6a312d903d..9b89042636 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerOutputFormatter.cs @@ -76,7 +76,10 @@ namespace Microsoft.AspNetCore.Mvc.Formatters _serializerSettings = new DataContractSerializerSettings(); - WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories(); + WrapperProviderFactories = new List() + { + new SerializableErrorWrapperProviderFactory(), + }; WrapperProviderFactories.Add(new EnumerableWrapperProviderFactory(WrapperProviderFactories)); _logger = loggerFactory?.CreateLogger(GetType()); diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs index 2c1ea40bbd..4d530a015b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs @@ -43,7 +43,10 @@ namespace Microsoft.AspNetCore.Mvc.Formatters SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml); SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax); - WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories(); + WrapperProviderFactories = new List + { + new SerializableErrorWrapperProviderFactory(), + }; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlSerializerMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerMvcOptionsSetup.cs similarity index 51% rename from src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlSerializerMvcOptionsSetup.cs rename to src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerMvcOptionsSetup.cs index 6c7546e332..1a57d167ce 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlSerializerMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerMvcOptionsSetup.cs @@ -2,32 +2,32 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal +namespace Microsoft.AspNetCore.Mvc.Formatters.Xml { /// /// A implementation which will add the /// XML serializer formatters to . /// - public class MvcXmlSerializerMvcOptionsSetup : IConfigureOptions + internal sealed class XmlSerializerMvcOptionsSetup : IConfigureOptions { + private readonly MvcXmlOptions _xmlOptions; private readonly ILoggerFactory _loggerFactory; /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// + /// . /// The . - public MvcXmlSerializerMvcOptionsSetup(ILoggerFactory loggerFactory) + public XmlSerializerMvcOptionsSetup( + IOptions xmlOptions, + ILoggerFactory loggerFactory) { - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - - _loggerFactory = loggerFactory; + _xmlOptions = xmlOptions?.Value ?? throw new ArgumentNullException(nameof(xmlOptions)); + _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } /// @@ -46,8 +46,14 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal MediaTypeHeaderValues.ApplicationXml); } - options.OutputFormatters.Add(new XmlSerializerOutputFormatter(_loggerFactory)); - options.InputFormatters.Add(new XmlSerializerInputFormatter(options)); + var inputFormatter = new XmlSerializerInputFormatter(options); + inputFormatter.WrapperProviderFactories.Add(new ProblemDetailsWrapperProviderFactory(_xmlOptions)); + options.InputFormatters.Add(inputFormatter); + + var outputFormatter = new XmlSerializerOutputFormatter(_loggerFactory); + outputFormatter.WrapperProviderFactories.Add(new ProblemDetailsWrapperProviderFactory(_xmlOptions)); + options.OutputFormatters.Add(outputFormatter); + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs index e24b2c9d20..c289972ea4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerOutputFormatter.cs @@ -73,7 +73,10 @@ namespace Microsoft.AspNetCore.Mvc.Formatters WriterSettings = writerSettings; - WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories(); + WrapperProviderFactories = new List + { + new SerializableErrorWrapperProviderFactory(), + }; WrapperProviderFactories.Add(new EnumerableWrapperProviderFactory(WrapperProviderFactories)); _logger = loggerFactory?.CreateLogger(GetType()); diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlDataContractSerializerMvcOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlDataContractSerializerMvcOptionsSetupTest.cs deleted file mode 100644 index 5219f0b97a..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlDataContractSerializerMvcOptionsSetupTest.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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.Extensions.Logging.Abstractions; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal -{ - public class MvcXmlDataContractSerializerMvcOptionsSetupTest - { - [Fact] - public void AddsFormatterMapping() - { - // Arrange - var optionsSetup = new MvcXmlDataContractSerializerMvcOptionsSetup(NullLoggerFactory.Instance); - var options = new MvcOptions(); - - // Act - optionsSetup.Configure(options); - - // Assert - var mappedContentType = options.FormatterMappings.GetMediaTypeMappingForFormat("xml"); - Assert.Equal("application/xml", mappedContentType); - } - - [Fact] - public void DoesNotOverrideExistingMapping() - { - // Arrange - var optionsSetup = new MvcXmlDataContractSerializerMvcOptionsSetup(NullLoggerFactory.Instance); - var options = new MvcOptions(); - options.FormatterMappings.SetMediaTypeMappingForFormat("xml", "text/xml"); - - // Act - optionsSetup.Configure(options); - - // Assert - var mappedContentType = options.FormatterMappings.GetMediaTypeMappingForFormat("xml"); - Assert.Equal("text/xml", mappedContentType); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlSerializerMvcOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlSerializerMvcOptionsSetupTest.cs deleted file mode 100644 index 0ff2072a4c..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlSerializerMvcOptionsSetupTest.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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.Extensions.Logging.Abstractions; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal -{ - public class MvcXmlSerializerMvcOptionsSetupTest - { - [Fact] - public void AddsFormatterMapping() - { - // Arrange - var optionsSetup = new MvcXmlSerializerMvcOptionsSetup(NullLoggerFactory.Instance); - var options = new MvcOptions(); - - // Act - optionsSetup.Configure(options); - - // Assert - var mappedContentType = options.FormatterMappings.GetMediaTypeMappingForFormat("xml"); - Assert.Equal("application/xml", mappedContentType); - } - - [Fact] - public void DoesNotOverrideExistingMapping() - { - // Arrange - var optionsSetup = new MvcXmlSerializerMvcOptionsSetup(NullLoggerFactory.Instance); - var options = new MvcOptions(); - options.FormatterMappings.SetMediaTypeMappingForFormat("xml", "text/xml"); - - // Act - optionsSetup.Configure(options); - - // Assert - var mappedContentType = options.FormatterMappings.GetMediaTypeMappingForFormat("xml"); - Assert.Equal("text/xml", mappedContentType); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetails21WrapperTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetails21WrapperTest.cs new file mode 100644 index 0000000000..933da23704 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetails21WrapperTest.cs @@ -0,0 +1,102 @@ +// 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 +{ +#pragma warning disable CS0618 // Type or member is obsolete + public class ProblemDetails21WrapperTest + { + [Fact] + public void ReadXml_ReadsProblemDetailsXml() + { + // Arrange + var xml = "" + + "" + + "Some title" + + "403" + + "Some instance" + + "Test Value 1" + + "<_x005B_key2_x005D_>Test Value 2" + + "Test Value 3" + + ""; + var serializer = new DataContractSerializer(typeof(ProblemDetails21Wrapper)); + + // Act + var value = serializer.ReadObject( + new MemoryStream(Encoding.UTF8.GetBytes(xml))); + + // Assert + var problemDetails = Assert.IsType(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 ProblemDetails21Wrapper(problemDetails); + var outputStream = new MemoryStream(); + var expectedContent = "" + + "" + + "Some detail" + + "Some title" + + "Test Value 1" + + "<_x005B_Key2_x005D_>Test Value 2" + + "Test Value 3" + + ""; + + // 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); + } + } +#pragma warning restore CS0618 // Type or member is obsolete + +} diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetailsWrapperProviderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetailsWrapperProviderFactoryTest.cs new file mode 100644 index 0000000000..21a62006b5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetailsWrapperProviderFactoryTest.cs @@ -0,0 +1,119 @@ +// 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 ProblemDetailsWrapperProviderFactoryTest + { + [Fact] + public void GetProvider_ReturnsNull_IfTypeDoesNotMatch() + { + // Arrange + var xmlOptions = new MvcXmlOptions(); + var providerFactory = new ProblemDetailsWrapperProviderFactory(xmlOptions); + var context = new WrapperProviderContext(typeof(SerializableError), isSerialization: true); + + // Act + var provider = providerFactory.GetProvider(context); + + // Assert + Assert.Null(provider); + } + + [Fact] + public void GetProvider_ReturnsWrapper_ForProblemDetails() + { + // Arrange + var xmlOptions = new MvcXmlOptions { AllowRfc7807CompliantProblemDetailsFormat = true }; + var providerFactory = new ProblemDetailsWrapperProviderFactory(xmlOptions); + var instance = new ProblemDetails(); + var context = new WrapperProviderContext(instance.GetType(), isSerialization: true); + + // Act + var provider = providerFactory.GetProvider(context); + + // Assert + var result = provider.Wrap(instance); + var wrapper = Assert.IsType(result); + Assert.Same(instance, wrapper.ProblemDetails); + } + + [Fact] + public void GetProvider_Returns21CompatibleWrapper_ForProblemDetails() + { + // Arrange + var xmlOptions = new MvcXmlOptions(); + var providerFactory = new ProblemDetailsWrapperProviderFactory(xmlOptions); + var instance = new ProblemDetails(); + var context = new WrapperProviderContext(instance.GetType(), isSerialization: true); + + // Act + var provider = providerFactory.GetProvider(context); + + // Assert + var result = provider.Wrap(instance); +#pragma warning disable CS0618 // Type or member is obsolete + var wrapper = Assert.IsType(result); +#pragma warning restore CS0618 // Type or member is obsolete + Assert.Same(instance, wrapper.ProblemDetails); + } + + [Fact] + public void GetProvider_ReturnsWrapper_ForValidationProblemDetails() + { + // Arrange + var xmlOptions = new MvcXmlOptions { AllowRfc7807CompliantProblemDetailsFormat = true }; + var providerFactory = new ProblemDetailsWrapperProviderFactory(xmlOptions); + var instance = new ValidationProblemDetails(); + var context = new WrapperProviderContext(instance.GetType(), isSerialization: true); + + // Act + var provider = providerFactory.GetProvider(context); + + // Assert + var result = provider.Wrap(instance); + var wrapper = Assert.IsType(result); + Assert.Same(instance, wrapper.ProblemDetails); + } + + [Fact] + public void GetProvider_Returns21CompatibleWrapper_ForValidationProblemDetails() + { + // Arrange + var xmlOptions = new MvcXmlOptions(); + var providerFactory = new ProblemDetailsWrapperProviderFactory(xmlOptions); + var instance = new ValidationProblemDetails(); + var context = new WrapperProviderContext(instance.GetType(), isSerialization: true); + + // Act + var provider = providerFactory.GetProvider(context); + + // Assert + var result = provider.Wrap(instance); +#pragma warning disable CS0618 // Type or member is obsolete + var wrapper = Assert.IsType(result); +#pragma warning restore CS0618 // Type or member is obsolete + Assert.Same(instance, wrapper.ProblemDetails); + } + + [Fact] + public void GetProvider_ReturnsNull_ForCustomProblemDetails() + { + // Arrange + var xmlOptions = new MvcXmlOptions(); + var providerFactory = new ProblemDetailsWrapperProviderFactory(xmlOptions); + var instance = new CustomProblemDetails(); + var context = new WrapperProviderContext(instance.GetType(), isSerialization: true); + + // Act + var provider = providerFactory.GetProvider(context); + + // Assert + Assert.Null(provider); + } + + private class CustomProblemDetails : ProblemDetails { } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetailsWrapperTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetailsWrapperTest.cs index 88db6289c9..055679f266 100644 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetailsWrapperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ProblemDetailsWrapperTest.cs @@ -56,7 +56,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml }); } - [Fact] public void WriteXml_WritesValidXml() { diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ValidationProblemDetails21WrapperTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ValidationProblemDetails21WrapperTest.cs new file mode 100644 index 0000000000..49b41afaf4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/ValidationProblemDetails21WrapperTest.cs @@ -0,0 +1,228 @@ +// 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 +{ +#pragma warning disable CS0618 // Type or member is obsolete + public class ValidationProblemDetails21WrapperTest + { + [Fact] + public void ReadXml_ReadsValidationProblemDetailsXml() + { + // Arrange + var xml = "" + + "" + + "Some title" + + "400" + + "Some instance" + + "Test Value 1" + + "<_x005B_key2_x005D_>Test Value 2" + + "" + + "Test error 1 Test error 2" + + "<_x005B_error2_x005D_>Test error 3" + + "Test error 4" + + "" + + ""; + var serializer = new DataContractSerializer(typeof(ValidationProblemDetails21Wrapper)); + + // Act + var value = serializer.ReadObject( + new MemoryStream(Encoding.UTF8.GetBytes(xml))); + + // Assert + var problemDetails = Assert.IsType(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 = "" + + "" + + "Some title" + + "400" + + "Some instance" + + "Test Value 1" + + "<_x005B_key2_x005D_>Test Value 2" + + ""; + var serializer = new DataContractSerializer(typeof(ValidationProblemDetails21Wrapper)); + + // Act + var value = serializer.ReadObject( + new MemoryStream(Encoding.UTF8.GetBytes(xml))); + + // Assert + var problemDetails = Assert.IsType(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 = "" + + "" + + "Some title" + + "400" + + "" + + ""; + var serializer = new DataContractSerializer(typeof(ValidationProblemDetails21Wrapper)); + + // Act + var value = serializer.ReadObject( + new MemoryStream(Encoding.UTF8.GetBytes(xml))); + + // Assert + var problemDetails = Assert.IsType(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 ValidationProblemDetails21Wrapper(problemDetails); + var outputStream = new MemoryStream(); + var expectedContent = "" + + "" + + "Some detail" + + "Some title" + + "Test Value 1" + + "<_x005B_Key2_x005D_>Test Value 2" + + "" + + "Test error 1 Test error 2" + + "<_x005B_error2_x005D_>Test error 3" + + "Test error 4" + + "" + + ""; + + // 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 ValidationProblemDetails21Wrapper(problemDetails); + var outputStream = new MemoryStream(); + var expectedContent = "" + + "" + + "Some detail" + + "Some title" + + "Test Value 1" + + "<_x005B_Key2_x005D_>Test Value 2" + + ""; + + // 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); + } + } +#pragma warning restore CS0618 // Type or member is obsolete +} diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/WrapperProviderFactoryExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/WrapperProviderFactoryExtensionsTest.cs deleted file mode 100644 index ca981f679a..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/WrapperProviderFactoryExtensionsTest.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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(factory), - factory => - { - var wrapperProviderFactory = Assert.IsType(factory); - Assert.Equal(typeof(ProblemDetails), wrapperProviderFactory.DeclaredType); - Assert.Equal(typeof(ProblemDetailsWrapper), wrapperProviderFactory.WrappingType); - }, - factory => - { - var wrapperProviderFactory = Assert.IsType(factory); - Assert.Equal(typeof(ValidationProblemDetails), wrapperProviderFactory.DeclaredType); - Assert.Equal(typeof(ValidationProblemDetailsWrapper), wrapperProviderFactory.WrappingType); - }); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/WrapperProviderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/WrapperProviderFactoryTest.cs deleted file mode 100644 index c067ed4ab9..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/WrapperProviderFactoryTest.cs +++ /dev/null @@ -1,63 +0,0 @@ -// 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); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerMvcOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerMvcOptionsSetupTest.cs new file mode 100644 index 0000000000..08c2b0aaf2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlDataContractSerializerMvcOptionsSetupTest.cs @@ -0,0 +1,71 @@ +// 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.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Formatters.Xml +{ + public class XmlDataContractSerializerMvcOptionsSetupTest + { + [Fact] + public void AddsFormatterMapping() + { + // Arrange + var optionsSetup = new XmlDataContractSerializerMvcOptionsSetup(Options.Create(new MvcXmlOptions()), NullLoggerFactory.Instance); + var options = new MvcOptions(); + + // Act + optionsSetup.Configure(options); + + // Assert + var mappedContentType = options.FormatterMappings.GetMediaTypeMappingForFormat("xml"); + Assert.Equal("application/xml", mappedContentType); + } + + [Fact] + public void DoesNotOverrideExistingMapping() + { + // Arrange + var optionsSetup = new XmlDataContractSerializerMvcOptionsSetup(Options.Create(new MvcXmlOptions()), NullLoggerFactory.Instance); + var options = new MvcOptions(); + options.FormatterMappings.SetMediaTypeMappingForFormat("xml", "text/xml"); + + // Act + optionsSetup.Configure(options); + + // Assert + var mappedContentType = options.FormatterMappings.GetMediaTypeMappingForFormat("xml"); + Assert.Equal("text/xml", mappedContentType); + } + + [Fact] + public void AddsInputFormatter() + { + // Arrange + var optionsSetup = new XmlDataContractSerializerMvcOptionsSetup(Options.Create(new MvcXmlOptions()), NullLoggerFactory.Instance); + var options = new MvcOptions(); + + // Act + optionsSetup.Configure(options); + + // Assert + Assert.IsType(Assert.Single(options.InputFormatters)); + } + + [Fact] + public void AddsOutputFormatter() + { + // Arrange + var optionsSetup = new XmlDataContractSerializerMvcOptionsSetup(Options.Create(new MvcXmlOptions()), NullLoggerFactory.Instance); + var options = new MvcOptions(); + + // Act + optionsSetup.Configure(options); + + // Assert + Assert.IsType(Assert.Single(options.OutputFormatters)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlSerializerMvcOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlSerializerMvcOptionsSetupTest.cs new file mode 100644 index 0000000000..d3b8790e64 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/XmlSerializerMvcOptionsSetupTest.cs @@ -0,0 +1,71 @@ +// 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.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Formatters.Xml +{ + public class XmlSerializerMvcOptionsSetupTest + { + [Fact] + public void AddsFormatterMapping() + { + // Arrange + var optionsSetup = new XmlSerializerMvcOptionsSetup(Options.Create(new MvcXmlOptions()), NullLoggerFactory.Instance); + var options = new MvcOptions(); + + // Act + optionsSetup.Configure(options); + + // Assert + var mappedContentType = options.FormatterMappings.GetMediaTypeMappingForFormat("xml"); + Assert.Equal("application/xml", mappedContentType); + } + + [Fact] + public void DoesNotOverrideExistingMapping() + { + // Arrange + var optionsSetup = new XmlSerializerMvcOptionsSetup(Options.Create(new MvcXmlOptions()), NullLoggerFactory.Instance); + var options = new MvcOptions(); + options.FormatterMappings.SetMediaTypeMappingForFormat("xml", "text/xml"); + + // Act + optionsSetup.Configure(options); + + // Assert + var mappedContentType = options.FormatterMappings.GetMediaTypeMappingForFormat("xml"); + Assert.Equal("text/xml", mappedContentType); + } + + [Fact] + public void AddsInputFormatter() + { + // Arrange + var optionsSetup = new XmlSerializerMvcOptionsSetup(Options.Create(new MvcXmlOptions()), NullLoggerFactory.Instance); + var options = new MvcOptions(); + + // Act + optionsSetup.Configure(options); + + // Assert + Assert.IsType(Assert.Single(options.InputFormatters)); + } + + [Fact] + public void AddsOutputFormatter() + { + // Arrange + var optionsSetup = new XmlSerializerMvcOptionsSetup(Options.Create(new MvcXmlOptions()), NullLoggerFactory.Instance); + var options = new MvcOptions(); + + // Act + optionsSetup.Configure(options); + + // Assert + Assert.IsType(Assert.Single(options.OutputFormatters)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerFormattersWrappingTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerFormattersWrappingTest.cs index d85de4c3c7..5c781c6652 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerFormattersWrappingTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerFormattersWrappingTest.cs @@ -2,12 +2,16 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Formatters.Xml; +using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Testing.xunit; +using XmlFormattersWebSite; using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -16,10 +20,12 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { public XmlDataContractSerializerFormattersWrappingTest(MvcTestFixture fixture) { - Client = fixture.CreateDefaultClient(); + Factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(builder => builder.UseStartup()); + Client = Factory.CreateDefaultClient(); } public HttpClient Client { get; } + public WebApplicationFactory Factory { get; } [ConditionalTheory] // Mono issue - https://github.com/aspnet/External/issues/18 @@ -254,6 +260,31 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests XmlAssert.Equal(expected, content); } + [Fact] + public async Task ProblemDetails_With21Behavior() + { + // Arrange + var expected = "" + + "instance" + + "404" + + "title" + + "correlation" + + "Account1 Account2" + + ""; + + var client = Factory + .WithWebHostBuilder(builder => builder.UseStartup()) + .CreateDefaultClient(); + + // 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() { @@ -302,5 +333,33 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var content = await response.Content.ReadAsStringAsync(); XmlAssert.Equal(expected, content); } + + [Fact] + public async Task ValidationProblemDetails_With21Behavior() + { + // Arrange + var expected = "" + + "some detail" + + "400" + + "One or more validation errors occurred." + + "some type" + + "correlation" + + "" + + "ErrorValue" + + "" + + ""; + + var client = Factory + .WithWebHostBuilder(builder => builder.UseStartup()) + .CreateDefaultClient(); + + // 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); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerFormattersWrappingTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerFormattersWrappingTest.cs index 33c76b98e8..542941cfb8 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerFormattersWrappingTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerFormattersWrappingTest.cs @@ -2,11 +2,15 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Formatters.Xml; +using Microsoft.AspNetCore.Mvc.Testing; +using XmlFormattersWebSite; using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -15,9 +19,11 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { public XmlSerializerFormattersWrappingTest(MvcTestFixture fixture) { - Client = fixture.CreateDefaultClient(); + Factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(builder => builder.UseStartup()); + Client = Factory.CreateDefaultClient(); } + public WebApplicationFactory Factory { get; } public HttpClient Client { get; } [Theory] @@ -208,6 +214,31 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests } } + [Fact] + public async Task ProblemDetails_With21Behavior() + { + // Arrange + var expected = "" + + "instance" + + "404" + + "title" + + "correlation" + + "Account1 Account2" + + ""; + + var client = Factory + .WithWebHostBuilder(builder => builder.UseStartup()) + .CreateDefaultClient(); + + // 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 ProblemDetails_WithExtensionMembers_IsSerialized() { @@ -277,5 +308,33 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests var content = await response.Content.ReadAsStringAsync(); XmlAssert.Equal(expected, content); } + + [Fact] + public async Task ValidationProblemDetails_With21Behavior() + { + // Arrange + var expected = "" + + "some detail" + + "400" + + "One or more validation errors occurred." + + "some type" + + "correlation" + + "" + + "ErrorValue" + + "" + + ""; + + var client = Factory + .WithWebHostBuilder(builder => builder.UseStartup()) + .CreateDefaultClient(); + + // 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); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TestMvcOptions.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TestMvcOptions.cs index ef11bd7f1c..615e8685cb 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TestMvcOptions.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/TestMvcOptions.cs @@ -5,12 +5,8 @@ using System; using System.Buffers; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; -using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal; using Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; diff --git a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs index 409605d730..ea544c1d26 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Formatters.Xml; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.DependencyInjection; @@ -26,7 +27,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest // Arrange var serviceCollection = new ServiceCollection(); AddHostingServices(serviceCollection); - serviceCollection.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_0); + serviceCollection + .AddMvc() + .AddXmlDataContractSerializerFormatters() + .SetCompatibilityVersion(CompatibilityVersion.Version_2_0); var services = serviceCollection.BuildServiceProvider(); @@ -36,6 +40,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest var razorPagesOptions = services.GetRequiredService>().Value; var apiBehaviorOptions = services.GetRequiredService>().Value; var razorViewEngineOptions = services.GetRequiredService>().Value; + var xmlOptions = services.GetRequiredService>().Value; // Assert Assert.False(mvcOptions.AllowCombiningAuthorizeFilters); @@ -50,6 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.True(apiBehaviorOptions.SuppressMapClientErrors); Assert.True(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); Assert.False(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); + Assert.False(xmlOptions.AllowRfc7807CompliantProblemDetailsFormat); } [Fact] @@ -58,7 +64,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest // Arrange var serviceCollection = new ServiceCollection(); AddHostingServices(serviceCollection); - serviceCollection.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + serviceCollection + .AddMvc() + .AddXmlDataContractSerializerFormatters() + .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); var services = serviceCollection.BuildServiceProvider(); @@ -68,6 +77,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest var razorPagesOptions = services.GetRequiredService>().Value; var apiBehaviorOptions = services.GetRequiredService>().Value; var razorViewEngineOptions = services.GetRequiredService>().Value; + var xmlOptions = services.GetRequiredService>().Value; // Assert Assert.True(mvcOptions.AllowCombiningAuthorizeFilters); @@ -82,6 +92,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.True(apiBehaviorOptions.SuppressMapClientErrors); Assert.True(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); Assert.False(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); + Assert.False(xmlOptions.AllowRfc7807CompliantProblemDetailsFormat); } [Fact] @@ -90,7 +101,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest // Arrange var serviceCollection = new ServiceCollection(); AddHostingServices(serviceCollection); - serviceCollection.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + serviceCollection + .AddMvc() + .AddXmlDataContractSerializerFormatters() + .SetCompatibilityVersion(CompatibilityVersion.Version_2_2); var services = serviceCollection.BuildServiceProvider(); @@ -100,6 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest var razorPagesOptions = services.GetRequiredService>().Value; var apiBehaviorOptions = services.GetRequiredService>().Value; var razorViewEngineOptions = services.GetRequiredService>().Value; + var xmlOptions = services.GetRequiredService>().Value; // Assert Assert.True(mvcOptions.AllowCombiningAuthorizeFilters); @@ -114,6 +129,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.False(apiBehaviorOptions.SuppressMapClientErrors); Assert.False(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); Assert.True(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); + Assert.True(xmlOptions.AllowRfc7807CompliantProblemDetailsFormat); } [Fact] @@ -122,7 +138,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest // Arrange var serviceCollection = new ServiceCollection(); AddHostingServices(serviceCollection); - serviceCollection.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Latest); + serviceCollection + .AddMvc() + .AddXmlDataContractSerializerFormatters() + .SetCompatibilityVersion(CompatibilityVersion.Latest); var services = serviceCollection.BuildServiceProvider(); @@ -132,6 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest var razorPagesOptions = services.GetRequiredService>().Value; var apiBehaviorOptions = services.GetRequiredService>().Value; var razorViewEngineOptions = services.GetRequiredService>().Value; + var xmlOptions = services.GetRequiredService>().Value; // Assert Assert.True(mvcOptions.AllowCombiningAuthorizeFilters); @@ -146,6 +166,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.False(apiBehaviorOptions.SuppressMapClientErrors); Assert.False(razorViewEngineOptions.AllowRecompilingViewsOnFileChange); Assert.True(razorPagesOptions.AllowDefaultHandlingForOptionsRequests); + Assert.True(xmlOptions.AllowRfc7807CompliantProblemDetailsFormat); } // This just does the minimum needed to be able to resolve these options. diff --git a/test/WebSites/XmlFormattersWebSite/Controllers/XmlDataContractApiController.cs b/test/WebSites/XmlFormattersWebSite/Controllers/XmlDataContractApiController.cs index dd8f228caa..fc4918943b 100644 --- a/test/WebSites/XmlFormattersWebSite/Controllers/XmlDataContractApiController.cs +++ b/test/WebSites/XmlFormattersWebSite/Controllers/XmlDataContractApiController.cs @@ -1,9 +1,12 @@ // 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.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace XmlFormattersWebSite { @@ -22,7 +25,10 @@ namespace XmlFormattersWebSite // 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()); + var mvcOptions = context.HttpContext.RequestServices.GetRequiredService>(); + var xmlFormatter = mvcOptions.Value.OutputFormatters.OfType().First(); + + objectResult.Formatters.Add(xmlFormatter); } } } diff --git a/test/WebSites/XmlFormattersWebSite/Controllers/XmlSerializedApiController.cs b/test/WebSites/XmlFormattersWebSite/Controllers/XmlSerializedApiController.cs index 6ee3ec4708..b6e39ad96b 100644 --- a/test/WebSites/XmlFormattersWebSite/Controllers/XmlSerializedApiController.cs +++ b/test/WebSites/XmlFormattersWebSite/Controllers/XmlSerializedApiController.cs @@ -1,9 +1,12 @@ // 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.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace XmlFormattersWebSite { @@ -22,7 +25,10 @@ namespace XmlFormattersWebSite // 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()); + var mvcOptions = context.HttpContext.RequestServices.GetRequiredService>(); + var xmlFormatter = mvcOptions.Value.OutputFormatters.OfType().First(); + + objectResult.Formatters.Add(xmlFormatter); } } } diff --git a/test/WebSites/XmlFormattersWebSite/Startup.cs b/test/WebSites/XmlFormattersWebSite/Startup.cs index 07bda1e37a..03cd9d9bd2 100644 --- a/test/WebSites/XmlFormattersWebSite/Startup.cs +++ b/test/WebSites/XmlFormattersWebSite/Startup.cs @@ -13,47 +13,85 @@ namespace XmlFormattersWebSite { public class Startup { + public virtual CompatibilityVersion CompatibilityVersion => CompatibilityVersion.Latest; + // Set up application services public void ConfigureServices(IServiceCollection services) { // Add MVC services to the services container services.AddMvc() - .SetCompatibilityVersion(CompatibilityVersion.Latest); + .AddXmlDataContractSerializerFormatters() + .AddXmlSerializerFormatters() + .SetCompatibilityVersion(CompatibilityVersion); services.Configure(options => { - options.InputFormatters.Clear(); - options.OutputFormatters.Clear(); - // Since both XmlSerializer and DataContractSerializer based formatters // have supported media types of 'application/xml' and 'text/xml', it // would be difficult for a test to choose a particular formatter based on - // request information (Ex: Accept header). - // So here we instead clear out the default supported media types and create new - // ones which are distinguishable between formatters. - var xmlSerializerInputFormatter = new XmlSerializerInputFormatter(new MvcOptions()); + // request information (Ex: Accept header). + // We'll configure the ones on MvcOptions to use a distinct set of content types. + + XmlSerializerInputFormatter xmlSerializerInputFormatter = null; + XmlSerializerOutputFormatter xmlSerializerOutputFormatter = null; + XmlDataContractSerializerInputFormatter dcsInputFormatter = null; + XmlDataContractSerializerOutputFormatter dcsOutputFormatter = null; + + for (var i = options.InputFormatters.Count - 1; i >= 0; i--) + { + switch (options.InputFormatters[i]) + { + case XmlSerializerInputFormatter formatter: + xmlSerializerInputFormatter = formatter; + break; + + case XmlDataContractSerializerInputFormatter formatter: + dcsInputFormatter = formatter; + break; + + default: + options.InputFormatters.RemoveAt(i); + break; + } + } + + for (var i = options.OutputFormatters.Count - 1; i >= 0; i--) + { + switch (options.OutputFormatters[i]) + { + case XmlSerializerOutputFormatter formatter: + xmlSerializerOutputFormatter = formatter; + break; + + case XmlDataContractSerializerOutputFormatter formatter: + dcsOutputFormatter = formatter; + break; + + default: + options.OutputFormatters.RemoveAt(i); + break; + } + } + xmlSerializerInputFormatter.SupportedMediaTypes.Clear(); - xmlSerializerInputFormatter.SupportedMediaTypes.Add( - new MediaTypeHeaderValue("application/xml-xmlser")); - xmlSerializerInputFormatter.SupportedMediaTypes.Add( - new MediaTypeHeaderValue("text/xml-xmlser")); + xmlSerializerInputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml-xmlser")); + xmlSerializerInputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml-xmlser")); + xmlSerializerInputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/problem+xml")); - var xmlSerializerOutputFormatter = new XmlSerializerOutputFormatter(); xmlSerializerOutputFormatter.SupportedMediaTypes.Clear(); - xmlSerializerOutputFormatter.SupportedMediaTypes.Add( - new MediaTypeHeaderValue("application/xml-xmlser")); - xmlSerializerOutputFormatter.SupportedMediaTypes.Add( - new MediaTypeHeaderValue("text/xml-xmlser")); + xmlSerializerOutputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml-xmlser")); + xmlSerializerOutputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml-xmlser")); + xmlSerializerOutputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/problem+xml")); - var dcsInputFormatter = new XmlDataContractSerializerInputFormatter(new MvcOptions()); dcsInputFormatter.SupportedMediaTypes.Clear(); dcsInputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml-dcs")); dcsInputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml-dcs")); + dcsInputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/problem+xml")); - var dcsOutputFormatter = new XmlDataContractSerializerOutputFormatter(); dcsOutputFormatter.SupportedMediaTypes.Clear(); dcsOutputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml-dcs")); dcsOutputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml-dcs")); + dcsOutputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/problem+xml")); options.InputFormatters.Add(dcsInputFormatter); options.InputFormatters.Add(xmlSerializerInputFormatter); diff --git a/test/WebSites/XmlFormattersWebSite/StartupWith21Compat.cs b/test/WebSites/XmlFormattersWebSite/StartupWith21Compat.cs new file mode 100644 index 0000000000..5b96407d99 --- /dev/null +++ b/test/WebSites/XmlFormattersWebSite/StartupWith21Compat.cs @@ -0,0 +1,13 @@ +// 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; + +namespace XmlFormattersWebSite +{ + public class StartupWith21Compat : Startup + { + public override CompatibilityVersion CompatibilityVersion => CompatibilityVersion.Version_2_1; + } +} +