Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Added new attribute ProducesResponseTypeAttribute to enable ApiExplor… #4269

Closed
wants to merge 1 commit into from
Closed
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
19 changes: 1 addition & 18 deletions src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,6 @@ public class ApiDescription
/// </summary>
public string RelativePath { get; set; }

/// <summary>
/// Gets or sets <see cref="ModelMetadata"/> for the <see cref="ResponseType"/> or null.
/// </summary>
/// <remarks>
/// Will be null if <see cref="ResponseType"/> is null.
/// </remarks>
public ModelMetadata ResponseModelMetadata { 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
/// <c>ProducesAttribute</c> on an action method to specify a response type.
/// </remarks>
public Type ResponseType { get; set; }

/// <summary>
/// Gets the list of possible formats for a response.
/// </summary>
Expand All @@ -76,6 +59,6 @@ public class ApiDescription
/// Will be empty if the action returns no response, or if the response type is unclear. Use
/// <c>ProducesAttribute</c> on an action method to specify a response type.
/// </remarks>
public IList<ApiResponseFormat> SupportedResponseFormats { get; } = new List<ApiResponseFormat>();
public IList<ApiResponseType> SupportedResponseTypes { get; } = new List<ApiResponseType>();
}
}
10 changes: 5 additions & 5 deletions src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseFormat.cs
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"/>.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential format?

/// </summary>
public class ApiResponseFormat
Copy link
Member

Choose a reason for hiding this comment

The 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; }
}
}
}
43 changes: 43 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseType.cs
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<c>null</c> if the ...

/// <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
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

The 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);
}
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}
}
}

Expand All @@ -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,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of making this a cross-product, maybe we should make each ApiResponseFormat have a list of (content type + formatter) - what do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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;
Expand Down Expand Up @@ -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.
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
/// <summary>
/// Provides a return type and a set of possible content types returned by a successful execution of the action.
/// Provides a return type, status code and a set of possible content types returned by a
/// successful execution of the action.
/// </summary>
public interface IApiResponseMetadataProvider
{
/// <summary>
/// Optimistic return type of the action.
/// Gets the optimistic return type of the action.
/// </summary>
Type Type { get; }

/// <summary>
/// Gets the HTTP status code of the response.
/// </summary>
int StatusCode { get; }

/// <summary>
/// Configures a collection of allowed content types which can be produced by the action.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
/// An <see cref="Formatters.IOutputFormatter"/> should implement this interface to expose metadata information
/// to an <c>IApiDescriptionProvider</c>.
/// </remarks>
public interface IApiResponseFormatMetadataProvider
public interface IApiResponseTypeMetadataProvider
{
/// <summary>
/// Gets a filtered list of content types which are supported by the <see cref="Formatters.IOutputFormatter"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <summary>
/// Writes an object to the output stream.
/// </summary>
public abstract class OutputFormatter : IOutputFormatter, IApiResponseFormatMetadataProvider
public abstract class OutputFormatter : IOutputFormatter, IApiResponseTypeMetadataProvider
{
/// <summary>
/// Gets the mutable collection of media type elements supported by
Expand Down
4 changes: 3 additions & 1 deletion src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Formatters.Internal;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Mvc
Expand Down Expand Up @@ -64,6 +64,8 @@ public ProducesAttribute(string contentType, params string[] additionalContentTy

public MediaTypeCollection ContentTypes { get; set; }

public int StatusCode => StatusCodes.Status200OK;

public override void OnResultExecuting(ResultExecutingContext context)
{
if (context == null)
Expand Down
Loading