Skip to content
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
77 changes: 71 additions & 6 deletions src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
Expand Down Expand Up @@ -49,7 +50,8 @@ public ICollection<ApiResponseType> GetApiResponseTypes(ControllerActionDescript
defaultErrorType = ((ProducesErrorResponseTypeAttribute)result!).Type;
}

var apiResponseTypes = GetApiResponseTypes(responseMetadataAttributes, runtimeReturnType, defaultErrorType);
var producesResponseMetadata = action.EndpointMetadata.OfType<IProducesResponseTypeMetadata>().ToList();
var apiResponseTypes = GetApiResponseTypes(responseMetadataAttributes, producesResponseMetadata, runtimeReturnType, defaultErrorType);
return apiResponseTypes;
}

Expand All @@ -72,23 +74,38 @@ private static List<IApiResponseMetadataProvider> GetResponseMetadataAttributes(

private ICollection<ApiResponseType> GetApiResponseTypes(
IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes,
IReadOnlyList<IProducesResponseTypeMetadata> producesResponseMetadata,
Type? type,
Type defaultErrorType)
{
var contentTypes = new MediaTypeCollection();
var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType<IApiResponseTypeMetadataProvider>();

var responseTypes = ReadResponseMetadata(
producesResponseMetadata,
type,
responseTypeMetadataProviders,
_modelMetadataProvider);

// Read response metadata from providers and
// overwrite responseTypes from the metadata based
// on the status code
var responseTypesFromProvider = ReadResponseMetadata(
responseMetadataAttributes,
type,
defaultErrorType,
contentTypes,
responseTypeMetadataProviders);

foreach (var responseType in responseTypesFromProvider)
{
responseTypes[responseType.Key] = responseType.Value;
}

// Set the default status only when no status has already been set explicitly
if (responseTypes.Count == 0 && type != null)
{
responseTypes.Add(new ApiResponseType
responseTypes.Add(StatusCodes.Status200OK, new ApiResponseType
{
StatusCode = StatusCodes.Status200OK,
Type = type,
Expand All @@ -105,16 +122,16 @@ private ICollection<ApiResponseType> GetApiResponseTypes(
contentTypes.Add((string)null!);
}

foreach (var apiResponse in responseTypes)
foreach (var apiResponse in responseTypes.Values)
{
CalculateResponseFormatForType(apiResponse, contentTypes, responseTypeMetadataProviders, _modelMetadataProvider);
}

return responseTypes;
return responseTypes.Values;
}

// Shared with EndpointMetadataApiDescriptionProvider
internal static List<ApiResponseType> ReadResponseMetadata(
internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes,
Type? type,
Type defaultErrorType,
Expand Down Expand Up @@ -195,7 +212,55 @@ internal static List<ApiResponseType> ReadResponseMetadata(
}
}

return results.Values.ToList();
return results;
}

internal static Dictionary<int, ApiResponseType> ReadResponseMetadata(
IReadOnlyList<IProducesResponseTypeMetadata> responseMetadata,
Type? type,
IEnumerable<IApiResponseTypeMetadataProvider>? responseTypeMetadataProviders = null,
IModelMetadataProvider? modelMetadataProvider = null)
{
var results = new Dictionary<int, ApiResponseType>();

foreach (var metadata in responseMetadata)
{
var statusCode = metadata.StatusCode;

var apiResponseType = new ApiResponseType
{
Type = metadata.Type,
StatusCode = statusCode,
};

if (apiResponseType.Type == typeof(void))
{
if (type != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created))
{
// Allow setting the response type from the return type of the method if it has
// not been set explicitly by the method.
apiResponseType.Type = type;
}
}

var attributeContentTypes = new MediaTypeCollection();
if (metadata.ContentTypes != null)
{
foreach (var contentType in metadata.ContentTypes)
{
attributeContentTypes.Add(contentType);
}
}

CalculateResponseFormatForType(apiResponseType, attributeContentTypes, responseTypeMetadataProviders, modelMetadataProvider);

if (apiResponseType.Type != null)
{
results[apiResponseType.StatusCode] = apiResponseType;
}
}

return results;
}

// Shared with EndpointMetadataApiDescriptionProvider
Expand Down
24 changes: 19 additions & 5 deletions src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,6 @@ private ApiDescription CreateApiDescription(
apiDescription.ParameterDescriptions.Add(parameter);
}

var requestMetadataAttributes = GetRequestMetadataAttributes(action);

var apiResponseTypes = _responseTypeProvider.GetApiResponseTypes(action);
foreach (var apiResponseType in apiResponseTypes)
{
Expand All @@ -127,7 +125,11 @@ private ApiDescription CreateApiDescription(
// could end up with duplicate data.
if (apiDescription.ParameterDescriptions.Count > 0)
{
var contentTypes = GetDeclaredContentTypes(requestMetadataAttributes);
// Get the most significant accepts metadata
var acceptsMetadata = action.EndpointMetadata.OfType<IAcceptsMetadata>().LastOrDefault();
var requestMetadataAttributes = GetRequestMetadataAttributes(action);

var contentTypes = GetDeclaredContentTypes(requestMetadataAttributes, acceptsMetadata);
foreach (var parameter in apiDescription.ParameterDescriptions)
{
if (parameter.Source == BindingSource.Body)
Expand Down Expand Up @@ -449,11 +451,23 @@ private IReadOnlyList<ApiRequestFormat> GetSupportedFormats(MediaTypeCollection
return results;
}

internal static MediaTypeCollection GetDeclaredContentTypes(IReadOnlyList<IApiRequestMetadataProvider>? requestMetadataAttributes)
internal static MediaTypeCollection GetDeclaredContentTypes(IReadOnlyList<IApiRequestMetadataProvider>? requestMetadataAttributes, IAcceptsMetadata? acceptsMetadata)
{
var contentTypes = new MediaTypeCollection();

// Walking the content types from the accepts metadata first
// to allow any RequestMetadataProvider to see or override any accepts metadata
// keeping the current behavior.
if (acceptsMetadata != null)
{
foreach (var contentType in acceptsMetadata.ContentTypes)
{
contentTypes.Add(contentType);
}
}

// Walk through all 'filter' attributes in order, and allow each one to see or override
// the results of the previous ones. This is similar to the execution path for content-negotiation.
var contentTypes = new MediaTypeCollection();
if (requestMetadataAttributes != null)
{
foreach (var metadataAttribute in requestMetadataAttributes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,11 @@ private static void AddSupportedResponseTypes(

var responseProviderMetadataTypes = ApiResponseTypeProvider.ReadResponseMetadata(
responseProviderMetadata, responseType, defaultErrorType, contentTypes);
var producesResponseMetadataTypes = ReadResponseMetadata(producesResponseMetadata, responseType);
var producesResponseMetadataTypes = ApiResponseTypeProvider.ReadResponseMetadata(producesResponseMetadata, responseType);

// We favor types added via the extension methods (which implements IProducesResponseTypeMetadata)
// over those that are added via attributes.
var responseMetadataTypes = producesResponseMetadataTypes.Values.Concat(responseProviderMetadataTypes);
var responseMetadataTypes = producesResponseMetadataTypes.Values.Concat(responseProviderMetadataTypes.Values);

if (responseMetadataTypes.Any())
{
Expand Down Expand Up @@ -397,51 +397,6 @@ private static void AddSupportedResponseTypes(
}
}

private static Dictionary<int, ApiResponseType> ReadResponseMetadata(
IReadOnlyList<IProducesResponseTypeMetadata> responseMetadata,
Type? type)
{
var results = new Dictionary<int, ApiResponseType>();

foreach (var metadata in responseMetadata)
{
var statusCode = metadata.StatusCode;

var apiResponseType = new ApiResponseType
{
Type = metadata.Type,
StatusCode = statusCode,
};

if (apiResponseType.Type == typeof(void))
{
if (type != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created))
{
// Allow setting the response type from the return type of the method if it has
// not been set explicitly by the method.
apiResponseType.Type = type;
}
}

var attributeContentTypes = new MediaTypeCollection();
if (metadata.ContentTypes != null)
{
foreach (var contentType in metadata.ContentTypes)
{
attributeContentTypes.Add(contentType);
}
}
ApiResponseTypeProvider.CalculateResponseFormatForType(apiResponseType, attributeContentTypes, responseTypeMetadataProviders: null, modelMetadataProvider: null);

if (apiResponseType.Type != null)
{
results[apiResponseType.StatusCode] = apiResponseType;
}
}

return results;
}

private static ApiResponseType CreateDefaultApiResponseType(Type responseType)
{
var apiResponseType = new ApiResponseType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,47 @@ public void GetApiDescription_PopulatesResponseType_ForActionResultOfT(string me
Assert.NotNull(responseType.ModelMetadata);
}

[Theory]
[InlineData(nameof(ReturnsResultOfProductWithEndpointMetadata))]
[InlineData(nameof(ReturnsTaskOfResultOfProductWithEndpointMetadata))]
public void GetApiDescription_PopulatesResponseType_ForResultOfT_WithEndpointMetadata(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
action.EndpointMetadata = new List<object>() { new ProducesResponseTypeMetadata(typeof(Product), 200) };

// Act
var descriptions = GetApiDescriptions(action);

// Assert
var description = Assert.Single(descriptions);
var responseType = Assert.Single(description.SupportedResponseTypes);
Assert.Equal(typeof(Product), responseType.Type);
Assert.NotNull(responseType.ModelMetadata);
}

[Theory]
[InlineData(nameof(ReturnsResultOfProductWithEndpointMetadata))]
[InlineData(nameof(ReturnsTaskOfResultOfProductWithEndpointMetadata))]
public void GetApiDescription_PopulatesResponseType_ForResultOfT_WithEndpointMetadata_PreferProducesAttribute(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
action.EndpointMetadata = new List<object>() { new ProducesResponseTypeMetadata(typeof(Product), 200) };
action.FilterDescriptors = new List<FilterDescriptor>{
new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(Customer), 200), FilterScope.Action)
};

// Act
var descriptions = GetApiDescriptions(action);

// Assert
var description = Assert.Single(descriptions);
var responseType = Assert.Single(description.SupportedResponseTypes);
Assert.Equal(typeof(Customer), responseType.Type);
Assert.NotNull(responseType.ModelMetadata);
}

[Theory]
[InlineData(nameof(ReturnsActionResultOfSequenceOfProducts))]
[InlineData(nameof(ReturnsTaskOfActionResultOfSequenceOfProducts))]
Expand Down Expand Up @@ -1212,6 +1253,24 @@ public void GetApiDescription_IncludesRequestFormats_FilteredByAttribute()
f => Assert.Equal("text/xml", f.MediaType.ToString()));
}

[Fact]
public void GetApiDescription_IncludesRequestFormats_FilteredByAcceptsMetadata()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsProduct_Body));
action.EndpointMetadata = new List<object>() { new XmlOnlyMetadata() };

// Act
var descriptions = GetApiDescriptions(action);

// Assert
var description = Assert.Single(descriptions);
Assert.Collection(
description.SupportedRequestFormats.OrderBy(f => f.MediaType.ToString()),
f => Assert.Equal("application/xml", f.MediaType.ToString()),
f => Assert.Equal("text/xml", f.MediaType.ToString()));
}

[Fact]
public void GetApiDescription_IncludesRequestFormats_FilteredByType()
{
Expand Down Expand Up @@ -1375,8 +1434,8 @@ public void GetApiDescription_ParameterDescription_SourceFromFormFile()
var action = CreateActionDescriptor(nameof(AcceptsFormFile));
action.FilterDescriptors = new[]
{
new FilterDescriptor(new ConsumesAttribute("multipart/form-data"), FilterScope.Action),
};
new FilterDescriptor(new ConsumesAttribute("multipart/form-data"), FilterScope.Action),
};

// Act
var descriptions = GetApiDescriptions(action);
Expand Down Expand Up @@ -2244,10 +2303,12 @@ private Product ReturnsProduct()
}

private ActionResult<Product> ReturnsActionResultOfProduct() => null;
private Http.HttpResults.Ok<Product> ReturnsResultOfProductWithEndpointMetadata() => null;

private ActionResult<IEnumerable<Product>> ReturnsActionResultOfSequenceOfProducts() => null;

private Task<ActionResult<Product>> ReturnsTaskOfActionResultOfProduct() => null;
private Task<Http.HttpResults.Ok<Product>> ReturnsTaskOfResultOfProductWithEndpointMetadata() => null;

private Task<ActionResult<IEnumerable<Product>>> ReturnsTaskOfActionResultOfSequenceOfProducts() => null;

Expand Down Expand Up @@ -2657,4 +2718,13 @@ private class FromFormFileAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource => BindingSource.FormFile;
}

private class XmlOnlyMetadata : Http.Metadata.IAcceptsMetadata
{
public IReadOnlyList<string> ContentTypes => new[] { "text/xml", "application/xml" };

public Type RequestType => null;

public bool IsOptional => false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public ApiBehaviorApplicationModelProvider(
ActionModelConventions = new List<IActionModelConvention>()
{
new ApiVisibilityConvention(),
new EndpointMetadataConvention(serviceProvider)
};

if (!options.SuppressMapClientErrors)
Expand Down
Loading