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

Commit

Permalink
Fixes #3818 - Support Consumes in ApiExplorer
Browse files Browse the repository at this point in the history
This change adds a list of ApiRequestFormat objects to ApiDescription
object which include the content type and formatter for each supported
content type which can be understood by the action.

Computation is aware of the [Consumes] attribute via the
IApiRequestMetadataProvider metadata interface, and aware of Input
Formatters via the new IApiRequestFormatMetadataProvider interface.

This algorithm is essentially the same as what we do for
produces/output-formatters. We iterate the filters and ask them what
content types they think are supported. Then we cross check that list with
the formatters, and ask them which from that list are supported. If no
[Consumes] filters are used, the formatters will include everything they
support by default.

This feature and data is only available when an action has a [FromBody]
parameter, which will naturally exclude actions that handle GET or DELETE
and don't process the body.
  • Loading branch information
rynowak committed Jan 8, 2016
1 parent bf1fcf6 commit 16b1fd3
Show file tree
Hide file tree
Showing 11 changed files with 488 additions and 58 deletions.
43 changes: 21 additions & 22 deletions src/Microsoft.AspNet.Mvc.ApiExplorer/ApiDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,45 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
public class ApiDescription
{
/// <summary>
/// Creates a new instance of <see cref="ApiDescription"/>.
/// </summary>
public ApiDescription()
{
Properties = new Dictionary<object, object>();
ParameterDescriptions = new List<ApiParameterDescription>();
SupportedResponseFormats = new List<ApiResponseFormat>();
}

/// <summary>
/// The <see cref="ActionDescriptor"/> for this api.
/// Gets or sets <see cref="ActionDescriptor"/> for this api.
/// </summary>
public ActionDescriptor ActionDescriptor { get; set; }

/// <summary>
/// The group name for this api.
/// Gets or sets group name for this api.
/// </summary>
public string GroupName { get; set; }

/// <summary>
/// The supported HTTP method for this api, or null if all HTTP methods are supported.
/// Gets or sets the supported HTTP method for this api, or null if all HTTP methods are supported.
/// </summary>
public string HttpMethod { get; set; }

/// <summary>
/// The list of <see cref="ApiParameterDescription"/> for this api.
/// Gets a list of <see cref="ApiParameterDescription"/> for this api.
/// </summary>
public IList<ApiParameterDescription> ParameterDescriptions { get; private set; }
public IList<ApiParameterDescription> ParameterDescriptions { get; } = new List<ApiParameterDescription>();

/// <summary>
/// Stores arbitrary metadata properties associated with the <see cref="ApiDescription"/>.
/// Gets arbitrary metadata properties associated with the <see cref="ApiDescription"/>.
/// </summary>
public IDictionary<object, object> Properties { get; private set; }
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();

/// <summary>
/// The relative url path template (relative to application root) for this api.
/// Gets or sets relative url path template (relative to application root) for this api.
/// </summary>
public string RelativePath { get; set; }

/// <summary>
/// The <see cref="ModelMetadata"/> for the <see cref="ResponseType"/> or null.
/// 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>
/// The CLR data type of the response or null.
/// 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
Expand All @@ -71,12 +61,21 @@ public ApiDescription()
public Type ResponseType { get; set; }

/// <summary>
/// A list of possible formats for a response.
/// Gets the list of possible formats for a response.
/// </summary>
/// <remarks>
/// 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<ApiRequestFormat> SupportedRequestFormats { get; } = new List<ApiRequestFormat>();

/// <summary>
/// Gets the list of possible formats for a response.
/// </summary>
/// <remarks>
/// 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; private set; }
public IList<ApiResponseFormat> SupportedResponseFormats { get; } = new List<ApiResponseFormat>();
}
}
24 changes: 24 additions & 0 deletions src/Microsoft.AspNet.Mvc.ApiExplorer/ApiRequestFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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.AspNet.Mvc.Formatters;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNet.Mvc.ApiExplorer
{
/// <summary>
/// A possible format for the body of a request.
/// </summary>
public class ApiRequestFormat
{
/// <summary>
/// The formatter used to read this request.
/// </summary>
public IInputFormatter Formatter { get; set; }

/// <summary>
/// The media type of the request.
/// </summary>
public MediaTypeHeaderValue MediaType { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace Microsoft.AspNet.Mvc.ApiExplorer
/// </summary>
public class DefaultApiDescriptionProvider : IApiDescriptionProvider
{
private readonly IList<IInputFormatter> _inputFormatters;
private readonly IList<IOutputFormatter> _outputFormatters;
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IInlineConstraintResolver _constraintResolver;
Expand All @@ -41,6 +42,7 @@ public DefaultApiDescriptionProvider(
IInlineConstraintResolver constraintResolver,
IModelMetadataProvider modelMetadataProvider)
{
_inputFormatters = optionsAccessor.Value.InputFormatters;
_outputFormatters = optionsAccessor.Value.OutputFormatters;
_constraintResolver = constraintResolver;
_modelMetadataProvider = modelMetadataProvider;
Expand Down Expand Up @@ -102,6 +104,7 @@ private ApiDescription CreateApiDescription(
apiDescription.ParameterDescriptions.Add(parameter);
}

var requestMetadataAttributes = GetRequestMetadataAttributes(action);
var responseMetadataAttributes = GetResponseMetadataAttributes(action);

// We only provide response info if we can figure out a type that is a user-data type.
Expand All @@ -126,19 +129,27 @@ private ApiDescription CreateApiDescription(

apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(runtimeReturnType);

var formats = GetResponseFormats(
action,
responseMetadataAttributes,
runtimeReturnType);

var formats = GetResponseFormats(action, responseMetadataAttributes, runtimeReturnType);
foreach (var format in formats)
{
apiDescription.SupportedResponseFormats.Add(format);
}
}

// It would be possible here to configure an action with multiple body parameters, in which case you
// could end up with duplicate data.
foreach (var parameter in apiDescription.ParameterDescriptions.Where(p => p.Source == BindingSource.Body))
{
var formats = GetRequestFormats(action, requestMetadataAttributes, parameter.Type);
foreach (var format in formats)
{
apiDescription.SupportedRequestFormats.Add(format);
}
}

return apiDescription;
}

private IList<ApiParameterDescription> GetParameters(ApiParameterContext context)
{
// First, get parameters from the model-binding/parameter-binding side of the world.
Expand Down Expand Up @@ -302,6 +313,56 @@ private string GetRelativePath(RouteTemplate parsedTemplate)
return string.Join("/", segments);
}

private IReadOnlyList<ApiRequestFormat> GetRequestFormats(
ControllerActionDescriptor action,
IApiRequestMetadataProvider[] requestMetadataAttributes,
Type type)
{
var results = new List<ApiRequestFormat>();

// 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 List<MediaTypeHeaderValue>();
if (requestMetadataAttributes != null)
{
foreach (var metadataAttribute in requestMetadataAttributes)
{
metadataAttribute.SetContentTypes(contentTypes);
}
}

if (contentTypes.Count == 0)
{
contentTypes.Add(null);
}

foreach (var contentType in contentTypes)
{
foreach (var formatter in _inputFormatters)
{
var requestFormatMetadataProvider = formatter as IApiRequestFormatMetadataProvider;
if (requestFormatMetadataProvider != null)
{
var supportedTypes = requestFormatMetadataProvider.GetSupportedContentTypes(contentType, type);

if (supportedTypes != null)
{
foreach (var supportedType in supportedTypes)
{
results.Add(new ApiRequestFormat()
{
Formatter = formatter,
MediaType = supportedType,
});
}
}
}
}
}

return results;
}

private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
ControllerActionDescriptor action,
IApiResponseMetadataProvider[] responseMetadataAttributes,
Expand Down Expand Up @@ -419,6 +480,23 @@ private Type GetRuntimeReturnType(Type declaredReturnType, IApiResponseMetadataP
return declaredReturnType;
}

private IApiRequestMetadataProvider[] GetRequestMetadataAttributes(ControllerActionDescriptor action)
{
if (action.FilterDescriptors == null)
{
return null;
}

// This technique for enumerating filters will intentionally ignore any filter that is an IFilterFactory
// while searching for a filter that implements IApiRequestMetadataProvider.
//
// The workaround for that is to implement the metadata interface on the IFilterFactory.
return action.FilterDescriptors
.Select(fd => fd.Filter)
.OfType<IApiRequestMetadataProvider>()
.ToArray();
}

private IApiResponseMetadataProvider[] GetResponseMetadataAttributes(ControllerActionDescriptor action)
{
if (action.FilterDescriptors == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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.Net.Http.Headers;

namespace Microsoft.AspNet.Mvc.ApiExplorer
{
/// <summary>
/// Provides metadata information about the request format to an <c>IApiDescriptionProvider</c>.
/// </summary>
/// <remarks>
/// An <see cref="Formatters.IInputFormatter"/> should implement this interface to expose metadata information
/// to an <c>IApiDescriptionProvider</c>.
/// </remarks>
public interface IApiRequestFormatMetadataProvider
{
/// <summary>
/// Gets a filtered list of content types which are supported by the <see cref="Formatters.IInputFormatter"/>
/// for the <paramref name="objectType"/> and <paramref name="contentType"/>.
/// </summary>
/// <param name="contentType">
/// The content type for which the supported content types are desired, or <c>null</c> if any content
/// type can be used.
/// </param>
/// <param name="objectType">
/// The <see cref="Type"/> for which the supported content types are desired.
/// </param>
/// <returns>Content types which are supported by the <see cref="Formatters.IInputFormatter"/>.</returns>
IReadOnlyList<MediaTypeHeaderValue> GetSupportedContentTypes(
MediaTypeHeaderValue contentType,
Type objectType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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.Collections.Generic;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNet.Mvc.ApiExplorer
{
/// <summary>
/// Provides a a set of possible content types than can be consumed by the action.
/// </summary>
public interface IApiRequestMetadataProvider
{
/// <summary>
/// Configures a collection of allowed content types which can be consumed by the action.
/// </summary>
void SetContentTypes(IList<MediaTypeHeaderValue> contentTypes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public interface IApiResponseFormatMetadataProvider
{
/// <summary>
/// Gets a filtered list of content types which are supported by the <see cref="Formatters.IOutputFormatter"/>
/// for the <paramref name="declaredType"/> and <paramref name="contentType"/>.
/// for the <paramref name="objectType"/> and <paramref name="contentType"/>.
/// </summary>
/// <param name="contentType">
/// The content type for which the supported content types are desired, or <c>null</c> if any content
Expand Down
17 changes: 16 additions & 1 deletion src/Microsoft.AspNet.Mvc.Core/ConsumesAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.ActionConstraints;
using Microsoft.AspNet.Mvc.ApiExplorer;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.Net.Http.Headers;
Expand All @@ -16,7 +17,11 @@ namespace Microsoft.AspNet.Mvc
/// Specifies the allowed content types which can be used to select the action based on request's content-type.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ConsumesAttribute : Attribute, IResourceFilter, IConsumesActionConstraint
public class ConsumesAttribute :
Attribute,
IResourceFilter,
IConsumesActionConstraint,
IApiRequestMetadataProvider
{
public static readonly int ConsumesActionConstraintOrder = 200;

Expand Down Expand Up @@ -184,5 +189,15 @@ private List<MediaTypeHeaderValue> GetContentTypes(string firstArg, string[] arg

return contentTypes;
}

/// <inheritdoc />
public void SetContentTypes(IList<MediaTypeHeaderValue> contentTypes)
{
contentTypes.Clear();
foreach (var contentType in ContentTypes)
{
contentTypes.Add(contentType);
}
}
}
}
Loading

0 comments on commit 16b1fd3

Please sign in to comment.