Skip to content

Media type application/problem+json lost in combination with ProducesAttribute #19510 #30367

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
3 commits merged into from
Feb 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/Mvc/Mvc.Core/src/Infrastructure/ObjectResultExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,19 +151,16 @@ private Task ExecuteAsyncCore(ActionContext context, ObjectResult result, Type?
private static void InferContentTypes(ActionContext context, ObjectResult result)
{
Debug.Assert(result.ContentTypes != null);
if (result.ContentTypes.Count != 0)
{
return;
}

// If the user sets the content type both on the ObjectResult (example: by Produces) and Response object,
// then the one set on ObjectResult takes precedence over the Response object
var responseContentType = context.HttpContext.Response.ContentType;
if (!string.IsNullOrEmpty(responseContentType))
if (result.ContentTypes.Count == 0 && !string.IsNullOrEmpty(responseContentType))
{
result.ContentTypes.Add(responseContentType);
}
else if (result.Value is ProblemDetails)

if (result.Value is ProblemDetails)
{
result.ContentTypes.Add("application/problem+json");
result.ContentTypes.Add("application/problem+xml");
Expand Down
95 changes: 84 additions & 11 deletions src/Mvc/Mvc.Core/test/Infrastructure/ObjectResultExecutorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,50 +114,123 @@ public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentTyp
}

[Fact]
public async Task ExecuteAsync_ForProblemDetailsValue_UsesSpecifiedContentType()
public async Task ExecuteAsync_WithResponseAndObjectResultContentType_ForProblemDetailsValue_UsesXMLContentType()
{
// Arrange
var executor = CreateExecutor();

var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext() { HttpContext = httpContext };
httpContext.Response.ContentType = "application/json";
httpContext.Response.ContentType = "application/xml"; // This will not be used

var result = new ObjectResult(new ProblemDetails())
{
ContentTypes = { "text/plain" },
ContentTypes = { "text/plain" }, // This will not be used
};
result.Formatters.Add(new TestXmlOutputFormatter());
result.Formatters.Add(new TestXmlOutputFormatter()); // This will be chosen based on the problem details content type
result.Formatters.Add(new TestJsonOutputFormatter());
result.Formatters.Add(new TestStringOutputFormatter()); // This will be chosen based on the content type
result.Formatters.Add(new TestStringOutputFormatter());

// Act
await executor.ExecuteAsync(actionContext, result);

// Assert
MediaTypeAssert.Equal("text/plain; charset=utf-8", httpContext.Response.ContentType);
MediaTypeAssert.Equal("application/problem+xml; charset=utf-8", httpContext.Response.ContentType);
}

[Fact]
public async Task ExecuteAsync_ForProblemDetailsValue_UsesResponseContentType()
public async Task ExecuteAsync_WithResponseContentType_ForProblemDetailsValue_UsesProblemDetailXMLContentType()
{
// Arrange
var executor = CreateExecutor();

var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext() { HttpContext = httpContext };
httpContext.Response.ContentType = "application/json";
httpContext.Response.ContentType = "application/json"; // This will not be used

var result = new ObjectResult(new ProblemDetails());
result.Formatters.Add(new TestXmlOutputFormatter()); // This will be chosen based on the problem details content type
result.Formatters.Add(new TestJsonOutputFormatter());
result.Formatters.Add(new TestStringOutputFormatter());

// Act
await executor.ExecuteAsync(actionContext, result);

// Assert
MediaTypeAssert.Equal("application/problem+xml; charset=utf-8", httpContext.Response.ContentType);
}

[Fact]
public async Task ExecuteAsync_ForProblemDetailsValue_UsesProblemDetailsContentType()
{
// Arrange
var executor = CreateExecutor();

var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext() { HttpContext = httpContext };
httpContext.Response.ContentType = "application/json"; // This will not be used

var result = new ObjectResult(new ProblemDetails());
result.Formatters.Add(new TestXmlOutputFormatter()); // This will be chosen based on the problem details content type
result.Formatters.Add(new TestJsonOutputFormatter());
result.Formatters.Add(new TestStringOutputFormatter());

// Act
await executor.ExecuteAsync(actionContext, result);

// Assert
MediaTypeAssert.Equal("application/problem+xml; charset=utf-8", httpContext.Response.ContentType);
}

[Fact]
public async Task ExecuteAsync_ForProblemDetailsValue_UsesProblemDetailsJsonContentType_BasedOnAcceptHeader()
{
// Arrange
var executor = CreateExecutor();

var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext() { HttpContext = httpContext };
httpContext.Request.Headers[HeaderNames.Accept] = "application/json"; // This will not be used
httpContext.Response.ContentType = "application/xml"; // This will not be used

var result = new ObjectResult(new ProblemDetails())
{
ContentTypes = { "text/plain" }, // This will not be used
};
result.Formatters.Add(new TestJsonOutputFormatter()); // This will be chosen based on the Accept Headers "application/json"
result.Formatters.Add(new TestXmlOutputFormatter());
result.Formatters.Add(new TestJsonOutputFormatter()); // This will be chosen based on the response content type
result.Formatters.Add(new TestStringOutputFormatter());

// Act
await executor.ExecuteAsync(actionContext, result);

// Assert
MediaTypeAssert.Equal("application/json; charset=utf-8", httpContext.Response.ContentType);
MediaTypeAssert.Equal("application/problem+json; charset=utf-8", httpContext.Response.ContentType);
}

[Fact]
public async Task ExecuteAsync_ForProblemDetailsValue_UsesProblemDetailsXMLContentType_BasedOnAcceptHeader()
{
// Arrange
var executor = CreateExecutor();

var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext() { HttpContext = httpContext };
httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used

var result = new ObjectResult(new ProblemDetails())
{
ContentTypes = { "text/plain" }, // This will not be used
};
result.Formatters.Add(new TestJsonOutputFormatter());
result.Formatters.Add(new TestXmlOutputFormatter()); // This will be chosen based on the Accept Headers "application/xml"
result.Formatters.Add(new TestStringOutputFormatter());

// Act
await executor.ExecuteAsync(actionContext, result);

// Assert
MediaTypeAssert.Equal("application/problem+xml; charset=utf-8", httpContext.Response.ContentType);
}

[Fact]
Expand All @@ -170,7 +243,7 @@ public async Task ExecuteAsync_NoContentTypeProvidedForProblemDetails_UsesDefaul
var actionContext = new ActionContext() { HttpContext = httpContext };

var result = new ObjectResult(new ProblemDetails());
result.Formatters.Add(new TestXmlOutputFormatter()); // This will be chosen based on the implicitly added content type
result.Formatters.Add(new TestXmlOutputFormatter()); // This will be chosen based on the problem details added content type
result.Formatters.Add(new TestJsonOutputFormatter());
result.Formatters.Add(new TestStringOutputFormatter());

Expand Down