diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs index 56f14f8185..33f2b51eac 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs @@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal options, _jsonOptions)); - options.FormatterMappings.SetMediaTypeMappingForFormat("json", MediaTypeHeaderValue.Parse("application/json")); + options.FormatterMappings.SetMediaTypeMappingForFormat("json", MediaTypeHeaderValues.ApplicationJson); options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(IJsonPatchDocument))); options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(JToken))); diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs index dbe7a8ca82..c1cd77f685 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlDataContractSerializerMvcOptionsSetup.cs @@ -6,6 +6,7 @@ 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 { @@ -42,6 +43,16 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter(_loggerFactory)); options.InputFormatters.Add(new XmlDataContractSerializerInputFormatter(options)); + // Do not override any user mapping + var key = "xml"; + var mapping = options.FormatterMappings.GetMediaTypeMappingForFormat(key); + if (string.IsNullOrEmpty(mapping)) + { + options.FormatterMappings.SetMediaTypeMappingForFormat( + key, + MediaTypeHeaderValues.ApplicationXml); + } + options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider("System.Xml.Linq.XObject")); options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider("System.Xml.XmlNode")); } diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlSerializerMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlSerializerMvcOptionsSetup.cs index e2b75d7f89..6c7546e332 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlSerializerMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/Internal/MvcXmlSerializerMvcOptionsSetup.cs @@ -4,6 +4,7 @@ using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal { @@ -35,6 +36,16 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal /// The . public void Configure(MvcOptions options) { + // Do not override any user mapping + var key = "xml"; + var mapping = options.FormatterMappings.GetMediaTypeMappingForFormat(key); + if (string.IsNullOrEmpty(mapping)) + { + options.FormatterMappings.SetMediaTypeMappingForFormat( + key, + MediaTypeHeaderValues.ApplicationXml); + } + options.OutputFormatters.Add(new XmlSerializerOutputFormatter(_loggerFactory)); options.InputFormatters.Add(new XmlSerializerInputFormatter(options)); } diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlDataContractSerializerMvcOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlDataContractSerializerMvcOptionsSetupTest.cs new file mode 100644 index 0000000000..5219f0b97a --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlDataContractSerializerMvcOptionsSetupTest.cs @@ -0,0 +1,42 @@ +// 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 new file mode 100644 index 0000000000..0ff2072a4c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Xml.Test/Internal/MvcXmlSerializerMvcOptionsSetupTest.cs @@ -0,0 +1,42 @@ +// 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.FunctionalTests/ContentNegotiationTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ContentNegotiationTest.cs index 9116b47831..75a684c4a4 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ContentNegotiationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ContentNegotiationTest.cs @@ -450,13 +450,12 @@ END:VCARD [Fact] public async Task ProducesAttribute_And_FormatFilterAttribute_Conflicting() { - // Arrange - var expectedContentType = MediaTypeHeaderValue.Parse("application/json"); - - // Act - var response = await Client.GetAsync("http://localhost/FormatFilter/MethodWithFormatFilter.json"); + // Arrange & Act + var response = await Client.GetAsync( + "http://localhost/FormatFilter/ProducesTakesPrecedenceOverUserSuppliedFormatMethod?format=json"); // Assert + // Explicit content type set by the developer takes precedence over the format requested by the end user Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -464,7 +463,8 @@ END:VCARD public async Task ProducesAttribute_And_FormatFilterAttribute_Collaborating() { // Arrange & Act - var response = await Client.GetAsync("http://localhost/FormatFilter/MethodWithFormatFilter"); + var response = await Client.GetAsync( + "http://localhost/FormatFilter/ProducesTakesPrecedenceOverUserSuppliedFormatMethod"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); @@ -508,5 +508,24 @@ END:VCARD var contact = xmlDeserializer.ReadObject(bodyStream) as Contact; Assert.Equal("Jason Ecsemelle", contact.Name); } + + [Fact] + public async Task FormatFilter_XmlAsFormat_ReturnsXml() + { + // Arrange + var expectedBody = "" + + "John"; + + // Act + var response = await Client.GetAsync( + "http://localhost/FormatFilter/CustomerInfo?format=xml"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/xml; charset=utf-8", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedBody, body); + } } } \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Controllers/ContentNegotiation/FormatFilterController.cs b/test/WebSites/BasicWebSite/Controllers/ContentNegotiation/FormatFilterController.cs index 34a1b22136..ec617bdf3e 100644 --- a/test/WebSites/BasicWebSite/Controllers/ContentNegotiation/FormatFilterController.cs +++ b/test/WebSites/BasicWebSite/Controllers/ContentNegotiation/FormatFilterController.cs @@ -7,24 +7,39 @@ using Microsoft.AspNetCore.Mvc.Filters; namespace BasicWebSite.Controllers.ContentNegotiation { - [Produces("application/FormatFilterController")] public class FormatFilterController : Controller { - public override void OnActionExecuted(ActionExecutedContext context) - { - var result = context.Result as ObjectResult; - if (result != null) - { - result.Formatters.Add(new CustomFormatter("application/FormatFilterController")); - } - - base.OnActionExecuted(context); - } - + [Produces("application/FormatFilterController")] [FormatFilter] - public string MethodWithFormatFilter() + [CustomFormatterActionFilter] + public string ProducesTakesPrecedenceOverUserSuppliedFormatMethod() { return "MethodWithFormatFilter"; } + + [HttpGet] + [FormatFilter] + public Customer CustomerInfo() + { + return new Customer() { Name = "John" }; + } + + private class CustomFormatterActionFilter : ActionFilterAttribute + { + public override void OnActionExecuted(ActionExecutedContext context) + { + var result = context.Result as ObjectResult; + if (result != null) + { + result.Formatters.Add(new CustomFormatter("application/FormatFilterController")); + } + } + } + + public class Customer + { + public string Name { get; set; } + } } + } \ No newline at end of file