-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Added new attribute ProducesResponseTypeAttribute to enable ApiExplor… #4269
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,23 @@ | ||
// 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. | ||
|
||
using Microsoft.AspNetCore.Mvc.Formatters; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer | ||
{ | ||
/// <summary> | ||
/// Represents a possible format for the body of a response. | ||
/// Possible format for an <see cref="ApiResponseType"/>. | ||
/// </summary> | ||
public class ApiResponseFormat | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs new docs yo |
||
{ | ||
/// <summary> | ||
/// The formatter used to output this response. | ||
/// Gets or sets the formatter used to output this response. | ||
/// </summary> | ||
public IOutputFormatter Formatter { get; set; } | ||
|
||
/// <summary> | ||
/// The media type of the response. | ||
/// Gets or sets the media type of the response. | ||
/// </summary> | ||
public string MediaType { get; set; } | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// 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 System; | ||
using System.Collections.Generic; | ||
using Microsoft.AspNetCore.Mvc.ModelBinding; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer | ||
{ | ||
/// <summary> | ||
/// Possible type of the response body which is formatted by <see cref="ApiResponseFormats"/>. | ||
/// </summary> | ||
public class ApiResponseType | ||
{ | ||
/// <summary> | ||
/// Gets or sets the response formats supported by this type. | ||
/// </summary> | ||
public IList<ApiResponseFormat> ApiResponseFormats { get; set; } = new List<ApiResponseFormat>(); | ||
|
||
/// <summary> | ||
/// Gets or sets <see cref="ModelBinding.ModelMetadata"/> for the <see cref="Type"/> or null. | ||
/// </summary> | ||
/// <remarks> | ||
/// Will be null if <see cref="Type"/> is null or void. | ||
/// </remarks> | ||
public ModelMetadata ModelMetadata { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the CLR data type of the response or null. | ||
/// </summary> | ||
/// <remarks> | ||
/// Will be null if the action returns no response, or if the response type is unclear. Use | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
/// <see cref="ProducesAttribute"/> or <see cref="ProducesResponseTypeAttribute"/> on an action method | ||
/// to specify a response type. | ||
/// </remarks> | ||
public Type Type { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the HTTP response status code. | ||
/// </summary> | ||
public int StatusCode { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
using System.Reflection; | ||
#endif | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc.Abstractions; | ||
using Microsoft.AspNetCore.Mvc.Controllers; | ||
using Microsoft.AspNetCore.Mvc.Formatters; | ||
|
@@ -112,37 +113,20 @@ private ApiDescription CreateApiDescription( | |
// Void /Task object/IActionResult will result in no data. | ||
var declaredReturnType = GetDeclaredReturnType(action); | ||
|
||
// Now 'simulate' an action execution. This attempts to figure out to the best of our knowledge | ||
// what the logical data type is using filters. | ||
var runtimeReturnType = GetRuntimeReturnType(declaredReturnType, responseMetadataAttributes); | ||
var runtimeReturnType = GetRuntimeReturnType(declaredReturnType); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment is out of date now. It doesn't do any of these things. |
||
|
||
// We might not be able to figure out a good runtime return type. If that's the case we don't | ||
// provide any information about outputs. The workaround is to attribute the action. | ||
if (runtimeReturnType == typeof(void)) | ||
var apiResponseTypes = GetApiResponseTypes(action, responseMetadataAttributes, runtimeReturnType); | ||
foreach (var apiResponseType in apiResponseTypes) | ||
{ | ||
// As a special case, if the return type is void - we want to surface that information | ||
// specifically, but nothing else. This can be overridden with a filter/attribute. | ||
apiDescription.ResponseType = runtimeReturnType; | ||
} | ||
else if (runtimeReturnType != null) | ||
{ | ||
apiDescription.ResponseType = runtimeReturnType; | ||
|
||
apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(runtimeReturnType); | ||
|
||
var formats = GetResponseFormats(action, responseMetadataAttributes, runtimeReturnType); | ||
foreach (var format in formats) | ||
{ | ||
apiDescription.SupportedResponseFormats.Add(format); | ||
} | ||
apiDescription.SupportedResponseTypes.Add(apiResponseType); | ||
} | ||
|
||
// It would be possible here to configure an action with multiple body parameters, in which case you | ||
// could end up with duplicate data. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we keep this special case? |
||
foreach (var parameter in apiDescription.ParameterDescriptions.Where(p => p.Source == BindingSource.Body)) | ||
{ | ||
var formats = GetRequestFormats(action, requestMetadataAttributes, parameter.Type); | ||
foreach (var format in formats) | ||
var requestFormats = GetRequestFormats(action, requestMetadataAttributes, parameter.Type); | ||
foreach (var format in requestFormats) | ||
{ | ||
apiDescription.SupportedRequestFormats.Add(format); | ||
} | ||
|
@@ -364,13 +348,24 @@ private IReadOnlyList<ApiRequestFormat> GetRequestFormats( | |
return results; | ||
} | ||
|
||
private IReadOnlyList<ApiResponseFormat> GetResponseFormats( | ||
private IReadOnlyList<ApiResponseType> GetApiResponseTypes( | ||
ControllerActionDescriptor action, | ||
IApiResponseMetadataProvider[] responseMetadataAttributes, | ||
Type type) | ||
{ | ||
var results = new List<ApiResponseFormat>(); | ||
var results = new List<ApiResponseType>(); | ||
|
||
// Build list of all possible return types (and status codes) for an action. | ||
var objectTypes = new Dictionary<int, Type>(); | ||
|
||
if (type != null && type != typeof(void)) | ||
{ | ||
// This return type can be overriden by any response metadata | ||
// attributes later if the user wishes to. | ||
objectTypes[StatusCodes.Status200OK] = type; | ||
} | ||
|
||
// Get the content type that the action explicitly set to support. | ||
// 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(); | ||
|
@@ -379,6 +374,11 @@ private IReadOnlyList<ApiResponseFormat> GetResponseFormats( | |
foreach (var metadataAttribute in responseMetadataAttributes) | ||
{ | ||
metadataAttribute.SetContentTypes(contentTypes); | ||
|
||
if (metadataAttribute.Type != null) | ||
{ | ||
objectTypes[metadataAttribute.StatusCode] = metadataAttribute.Type; | ||
} | ||
} | ||
} | ||
|
||
|
@@ -387,28 +387,53 @@ private IReadOnlyList<ApiResponseFormat> GetResponseFormats( | |
contentTypes.Add((string)null); | ||
} | ||
|
||
foreach (var contentType in contentTypes) | ||
var responseTypeMetadataProviders = _outputFormatters.OfType<IApiResponseTypeMetadataProvider>(); | ||
|
||
foreach (var objectType in objectTypes) | ||
{ | ||
foreach (var formatter in _outputFormatters) | ||
if (objectType.Value == typeof(void)) | ||
{ | ||
var responseFormatMetadataProvider = formatter as IApiResponseFormatMetadataProvider; | ||
if (responseFormatMetadataProvider != null) | ||
results.Add(new ApiResponseType() | ||
{ | ||
var supportedTypes = responseFormatMetadataProvider.GetSupportedContentTypes(contentType, type); | ||
StatusCode = objectType.Key, | ||
Type = objectType.Value | ||
}); | ||
|
||
if (supportedTypes != null) | ||
continue; | ||
} | ||
|
||
var apiResponseType = new ApiResponseType() | ||
{ | ||
Type = objectType.Value, | ||
StatusCode = objectType.Key, | ||
ModelMetadata = _modelMetadataProvider.GetMetadataForType(objectType.Value) | ||
}; | ||
|
||
foreach (var contentType in contentTypes) | ||
{ | ||
foreach (var responseTypeMetadataProvider in responseTypeMetadataProviders) | ||
{ | ||
var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes( | ||
contentType, | ||
objectType.Value); | ||
|
||
if (formatterSupportedContentTypes == null) | ||
{ | ||
foreach (var supportedType in supportedTypes) | ||
continue; | ||
} | ||
|
||
foreach (var formatterSupportedContentType in formatterSupportedContentTypes) | ||
{ | ||
apiResponseType.ApiResponseFormats.Add(new ApiResponseFormat() | ||
{ | ||
results.Add(new ApiResponseFormat() | ||
{ | ||
Formatter = formatter, | ||
MediaType = supportedType, | ||
}); | ||
} | ||
Formatter = (IOutputFormatter)responseTypeMetadataProvider, | ||
MediaType = formatterSupportedContentType, | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of making this a cross-product, maybe we should make each There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this idea but probably there is one scenario where it might not work well. Each formatter looks at the type also to decide if they can support to write or not, so we having a list of types against a formatter would not be valid in this case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I spoke too soon. I will try making this change and see now. |
||
} | ||
} | ||
} | ||
|
||
results.Add(apiResponseType); | ||
} | ||
|
||
return results; | ||
|
@@ -445,28 +470,8 @@ private static Type GetTaskInnerTypeOrNull(Type type) | |
return genericType?.GenericTypeArguments[0]; | ||
} | ||
|
||
private Type GetRuntimeReturnType(Type declaredReturnType, IApiResponseMetadataProvider[] metadataAttributes) | ||
private Type GetRuntimeReturnType(Type declaredReturnType) | ||
{ | ||
// Walk through all of the filter attributes and allow them to set the type. This will execute them | ||
// in filter-order allowing the desired behavior for overriding. | ||
if (metadataAttributes != null) | ||
{ | ||
Type typeSetByAttribute = null; | ||
foreach (var metadataAttribute in metadataAttributes) | ||
{ | ||
if (metadataAttribute.Type != null) | ||
{ | ||
typeSetByAttribute = metadataAttribute.Type; | ||
} | ||
} | ||
|
||
// If one of the filters set a type, then trust it. | ||
if (typeSetByAttribute != null) | ||
{ | ||
return typeSetByAttribute; | ||
} | ||
} | ||
|
||
// If we get here, then a filter didn't give us an answer, so we need to figure out if we | ||
// want to use the declared return type. | ||
// | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential format
?