Content-Type negation doesn't work upgrading from netcoreapp3.1 to net6 ASP.NET Core

383 Views Asked by At

I'm (trying) to upgrade ASP.NET Core application from .NET Core App 3.1 to .NET 6 but one test fails that deserialize a Problem result. Reason for failing is that in .NET 6 the content type is application/problem+json whilst in .NET Core App 3.1 application/xml.

Have searched for any notes regarding this in migration document but can't find anything.

A repro is available in my GitHub and the controller is very simple

using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;

namespace ProblemDetailsXMLSerialization
{
    [ApiController]
    [Route("[controller]")]
    public class XmlController : ControllerBase
    {
        [HttpPost]
        [Produces(MediaTypeNames.Application.Xml)]
        [Consumes(MediaTypeNames.Application.Xml)]
        public IActionResult Xml()
        {
            return Problem();
        }
    }
}

// Test file
using Microsoft.AspNetCore.Mvc.Testing;
using ProblemDetailsXMLSerialization;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace TestProject1
{
    public class UnitTest1
    {
        [Fact]
        public async Task Test1()
        {
            // Arrange
            var application = new WebApplicationFactory<Startup>();
            var client = application.CreateClient();

            // Act
            const string xml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<note>
  <to>Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>Don't forget me this weekend!</body>
</note>";
            var content = new StringContent(xml, Encoding.UTF8, MediaTypeNames.Application.Xml);
            var response = await client.PostAsync("xml", content);

            // Assert
            Assert.Equal(MediaTypeNames.Application.Xml, response.Content.Headers.ContentType.MediaType);
            var responseString = await response.Content.ReadAsStringAsync();
        }
    }
}

Thanks

2

There are 2 best solutions below

1
pfx On BEST ANSWER

To get an XML response - matching your assert statement - you'll need to add an Accept HTTP header with value application/xml.

From the documentation:

Content negotiation occurs when the client specifies an Accept header. The default format used by ASP.NET Core is JSON.


var content = new StringContent(xml, Encoding.UTF8, MediaTypeNames.Application.Xml);
client.DefaultRequestHeaders.Add(
    "Accept", "application/xml"
    );
var response = await client.PostAsync("xml", content);

There are built-in strings for both Accept and application/xml.

client.DefaultRequestHeaders.Add(
    Microsoft.Net.Http.Headers.HeaderNames.Accept,  
    System.Net.Mime.MediaTypeNames.Application.Xml
    );

Setting that header to the DefaultRequestHeaders makes it being sent with every request made by that HttpClient instance.

In case you only want/need it for a single request, then use a HttpRequestMessage instance.

using (var request = new HttpRequestMessage(HttpMethod.Post, "xml"))
{
    request.Headers.Add("accept", "application/xml");
    request.Content = new StringContent(xml, Encoding.UTF8, MediaTypeNames.Application.Xml);
    var response = await client.SendAsync(request);

    var responseString = await response.Content.ReadAsStringAsync();
}

In either case, the responseString variable will contain an xml payload similar to below one.

<problem xmlns="urn:ietf:rfc:7807">
    <status>500</status>
    <title>An error occurred while processing your request.</title>
    <type>https://tools.ietf.org/html/rfc7231#section-6.6.1</type>
    <traceId>00-26c29d0830bd0a5a417e9bab9746bd23-3cfbc9589ffd8182-00</traceId>
</problem>
1
Swedish Zorro On

TLDR

To fix this change the order that the XmlOutput formatter is registered. Set it to first position. After services.AddControllers().AddXmlSerializerFormatters() in the startup set the XmlFormatter in first position. Haven't tried this code out but something like this should work:

services.AddControllers().AddXmlSerializerFormatters();
services.Configure<MvcOptions>(options => {
   var xmlFormatterIdx = options.OutputFormatters.length - 1; 
   options.OutputFormatters.Insert(0, 
options.OutputFormatters[xmlFormatterIdx]);
   options.OutputFormatters.RemoveAt(xmlFormatterIdx + 1);
});

Details

When using the "Produces" attribute then this should be the response type. In this case it seems to be a problem in how dotnet handles object results that contain a ProblemDetail response. So this is what I've seen happens when decompiling and checking the source code:

  1. The request starts and since we have the Produces attribute with application/xml then the content types for the result is application.xml only.

  2. When the return type of the ObjectResult is ProblemDetails is detected 2 new content types are appended to the end of the content types list: https://github.com/dotnet/dotnet/blob/b8bc661a7429baa89070f3bee636b7fbc1309489/src/aspnetcore/src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs#L152

  3. Now we have 3 possible content types for the response. The output format selector now picks the wrong formatter: https://github.com/dotnet/dotnet/blob/b8bc661a7429baa89070f3bee636b7fbc1309489/src/aspnetcore/src/Mvc/Mvc.Core/src/Infrastructure/DefaultOutputFormatterSelector.cs#L192

Conclusion

The produces attribute should override it all but it doesn't. The current implementation in dotnet gives the order of how output formatters are registered a higher priority.

Could be that the DefaultOutputformatter implementation should be done differently and use the content types of the object result as order of response types rather than how the output handlers are registered. Not sure on what the side effects could be but this could potentially a something for the dotnet team to look into