Use content-type specified by ProducesAttribute if no formatter supports it
This allows users to use `ProducesAttribute` to specify the content-type for action results such as FileStreamResult where the result determines the content type and the specified value is informational. Fixes https://github.com/aspnet/Mvc/issues/5701
This commit is contained in:
parent
f7da3503d6
commit
9c424b7b02
|
|
@ -135,12 +135,33 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
|
|
||||||
if (contentTypes.Count == 0)
|
if (contentTypes.Count == 0)
|
||||||
{
|
{
|
||||||
|
// None of the IApiResponseMetadataProvider specified a content type. This is common for actions that
|
||||||
|
// specify one or more ProducesResponseType but no ProducesAttribute. In this case, formatters will participate in conneg
|
||||||
|
// and respond to the incoming request.
|
||||||
|
// Querying IApiResponseTypeMetadataProvider.GetSupportedContentTypes with "null" should retrieve all supported
|
||||||
|
// content types that each formatter may respond in.
|
||||||
contentTypes.Add((string)null);
|
contentTypes.Add((string)null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var responseTypes = results.Values;
|
||||||
|
CalculateResponseFormats(responseTypes, contentTypes);
|
||||||
|
return responseTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateResponseFormats(ICollection<ApiResponseType> responseTypes, MediaTypeCollection declaredContentTypes)
|
||||||
|
{
|
||||||
var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType<IApiResponseTypeMetadataProvider>();
|
var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType<IApiResponseTypeMetadataProvider>();
|
||||||
|
|
||||||
foreach (var apiResponse in results.Values)
|
// Given the content-types that were declared for this action, determine the formatters that support the content-type for the given
|
||||||
|
// response type.
|
||||||
|
// 1. Responses that do not specify an type do not have any associated content-type. This usually is meant for status-code only responses such
|
||||||
|
// as return NotFound();
|
||||||
|
// 2. When a type is specified, use GetSupportedContentTypes to expand wildcards and get the range of content-types formatters support.
|
||||||
|
// 3. When no formatter supports the specified content-type, use the user specified value as is. This is useful in actions where the user
|
||||||
|
// dictates the content-type.
|
||||||
|
// e.g. [Produces("application/pdf")] Action() => FileStream("somefile.pdf", "applicaiton/pdf");
|
||||||
|
|
||||||
|
foreach (var apiResponse in responseTypes)
|
||||||
{
|
{
|
||||||
var responseType = apiResponse.Type;
|
var responseType = apiResponse.Type;
|
||||||
if (responseType == null || responseType == typeof(void))
|
if (responseType == null || responseType == typeof(void))
|
||||||
|
|
@ -150,8 +171,10 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
|
|
||||||
apiResponse.ModelMetadata = _modelMetadataProvider.GetMetadataForType(responseType);
|
apiResponse.ModelMetadata = _modelMetadataProvider.GetMetadataForType(responseType);
|
||||||
|
|
||||||
foreach (var contentType in contentTypes)
|
foreach (var contentType in declaredContentTypes)
|
||||||
{
|
{
|
||||||
|
var isSupportedContentType = false;
|
||||||
|
|
||||||
foreach (var responseTypeMetadataProvider in responseTypeMetadataProviders)
|
foreach (var responseTypeMetadataProvider in responseTypeMetadataProviders)
|
||||||
{
|
{
|
||||||
var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(
|
var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(
|
||||||
|
|
@ -163,6 +186,8 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSupportedContentType = true;
|
||||||
|
|
||||||
foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
|
foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
|
||||||
{
|
{
|
||||||
apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
|
apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
|
||||||
|
|
@ -172,10 +197,17 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSupportedContentType && contentType != null)
|
||||||
|
{
|
||||||
|
// No output formatter was found that supports this content type. Add the user specified content type as-is to the result.
|
||||||
|
apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
|
||||||
|
{
|
||||||
|
MediaType = contentType,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.Values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Type GetDeclaredReturnType(ControllerActionDescriptor action)
|
private Type GetDeclaredReturnType(ControllerActionDescriptor action)
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
Assert.False(responseType.IsDefaultResponse);
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
responseType.ApiResponseFormats,
|
responseType.ApiResponseFormats,
|
||||||
format => Assert.Equal("application/json", format.MediaType));
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", format.MediaType);
|
||||||
|
Assert.IsType<TestOutputFormatter>(format.Formatter);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
responseType =>
|
responseType =>
|
||||||
{
|
{
|
||||||
|
|
@ -106,7 +110,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
Assert.False(responseType.IsDefaultResponse);
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
responseType.ApiResponseFormats,
|
responseType.ApiResponseFormats,
|
||||||
format => Assert.Equal("application/json", format.MediaType));
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", format.MediaType);
|
||||||
|
Assert.IsType<TestOutputFormatter>(format.Formatter);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
responseType =>
|
responseType =>
|
||||||
{
|
{
|
||||||
|
|
@ -115,7 +123,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
Assert.False(responseType.IsDefaultResponse);
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
responseType.ApiResponseFormats,
|
responseType.ApiResponseFormats,
|
||||||
format => Assert.Equal("application/json", format.MediaType));
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", format.MediaType);
|
||||||
|
Assert.IsType<TestOutputFormatter>(format.Formatter);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
responseType =>
|
responseType =>
|
||||||
{
|
{
|
||||||
|
|
@ -156,7 +168,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
Assert.False(responseType.IsDefaultResponse);
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
responseType.ApiResponseFormats,
|
responseType.ApiResponseFormats,
|
||||||
format => Assert.Equal("application/json", format.MediaType));
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", format.MediaType);
|
||||||
|
Assert.IsType<TestOutputFormatter>(format.Formatter);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
responseType =>
|
responseType =>
|
||||||
{
|
{
|
||||||
|
|
@ -663,6 +679,36 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiResponseTypes_UsesContentTypeWithoutWildCard_WhenNoFormatterSupportsIt()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetUser));
|
||||||
|
actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("application/pdf"), FilterScope.Action));
|
||||||
|
|
||||||
|
var provider = GetProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = provider.GetApiResponseTypes(actionDescriptor);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
result.OrderBy(r => r.StatusCode),
|
||||||
|
responseType =>
|
||||||
|
{
|
||||||
|
Assert.Equal(200, responseType.StatusCode);
|
||||||
|
Assert.Equal(typeof(DerivedModel), responseType.Type);
|
||||||
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
|
Assert.Collection(
|
||||||
|
responseType.ApiResponseFormats,
|
||||||
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/pdf", format.MediaType);
|
||||||
|
Assert.Null(format.Formatter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static ApiResponseTypeProvider GetProvider()
|
private static ApiResponseTypeProvider GetProvider()
|
||||||
{
|
{
|
||||||
var mvcOptions = new MvcOptions
|
var mvcOptions = new MvcOptions
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
@ -829,17 +830,27 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
// Assert
|
// Assert
|
||||||
var description = Assert.Single(result);
|
var description = Assert.Single(result);
|
||||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||||
Assert.Equal(1, responseType.ResponseFormats.Count);
|
Assert.Collection(
|
||||||
|
responseType.ResponseFormats,
|
||||||
var responseFormat = responseType.ResponseFormats[0];
|
responseFormat =>
|
||||||
Assert.Equal("application/hal+json", responseFormat.MediaType);
|
{
|
||||||
Assert.Equal(typeof(JsonOutputFormatter).FullName, responseFormat.FormatterType);
|
Assert.Equal("application/hal+custom", responseFormat.MediaType);
|
||||||
|
Assert.Null(responseFormat.FormatterType);
|
||||||
|
},
|
||||||
|
responseFormat =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/hal+json", responseFormat.MediaType);
|
||||||
|
Assert.Equal(typeof(JsonOutputFormatter).FullName, responseFormat.FormatterType);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ApiExplorer_ResponseContentType_NoMatch()
|
public async Task ApiExplorer_ResponseContentType_NoMatch()
|
||||||
{
|
{
|
||||||
// Arrange & Act
|
// Arrange
|
||||||
|
var expectedMediaTypes = new[] { "application/custom", "text/hal+bson" };
|
||||||
|
|
||||||
|
// Act
|
||||||
var response = await Client.GetAsync("http://localhost/ApiExplorerResponseContentType/NoMatch");
|
var response = await Client.GetAsync("http://localhost/ApiExplorerResponseContentType/NoMatch");
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
|
@ -848,7 +859,11 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
// Assert
|
// Assert
|
||||||
var description = Assert.Single(result);
|
var description = Assert.Single(result);
|
||||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||||
Assert.Empty(responseType.ResponseFormats);
|
|
||||||
|
|
||||||
|
Assert.Equal(typeof(Product).FullName, responseType.ResponseType);
|
||||||
|
Assert.Equal(200, responseType.StatusCode);
|
||||||
|
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalTheory]
|
[ConditionalTheory]
|
||||||
|
|
@ -1147,6 +1162,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("multipart/form-data", requestFormat.MediaType);
|
Assert.Equal("multipart/form-data", requestFormat.MediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiBehavior_UsesContentTypeFromProducesAttribute_WhenNoFormatterSupportsIt()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMediaTypes = new[] { "application/pdf" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var body = await Client.GetStringAsync("ApiExplorerApiController/ProducesWithUnsupportedContentType");
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Collection(
|
||||||
|
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||||
|
responseType =>
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(Stream).FullName, responseType.ResponseType);
|
||||||
|
Assert.Equal(200, responseType.StatusCode);
|
||||||
|
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task ApiConvention_ForGetMethod_ReturningModel() => ApiConvention_ForGetMethod("GetProduct");
|
public Task ApiConvention_ForGetMethod_ReturningModel() => ApiConvention_ForGetMethod("GetProduct");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using System.IO;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace ApiExplorerWebSite
|
namespace ApiExplorerWebSite
|
||||||
{
|
{
|
||||||
|
|
@ -27,5 +28,8 @@ namespace ApiExplorerWebSite
|
||||||
public void ActionWithFormFileCollectionParameter(IFormFileCollection formFile)
|
public void ActionWithFormFileCollectionParameter(IFormFileCollection formFile)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Produces("application/pdf", Type = typeof(Stream))]
|
||||||
|
public IActionResult ProducesWithUnsupportedContentType() => null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue