aspnetcore/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvid...

454 lines
18 KiB
C#

// Copyright (c) Microsoft Open Technologies, Inc. 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 System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Implements a provider of <see cref="ApiDescription"/> for actions represented
/// by <see cref="ControllerActionDescriptor"/>.
/// </summary>
public class DefaultApiDescriptionProvider : INestedProvider<ApiDescriptionProviderContext>
{
private readonly IOutputFormattersProvider _formattersProvider;
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly IInlineConstraintResolver _constraintResolver;
/// <summary>
/// Creates a new instance of <see cref="DefaultApiDescriptionProvider"/>.
/// </summary>
/// <param name="formattersProvider">The <see cref="IOutputFormattersProvider"/>.</param>
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
public DefaultApiDescriptionProvider(
IOutputFormattersProvider formattersProvider,
IInlineConstraintResolver constraintResolver,
IModelMetadataProvider modelMetadataProvider)
{
_formattersProvider = formattersProvider;
_modelMetadataProvider = modelMetadataProvider;
_constraintResolver = constraintResolver;
}
/// <inheritdoc />
public int Order
{
get { return DefaultOrder.DefaultFrameworkSortOrder; }
}
/// <inheritdoc />
public void Invoke(ApiDescriptionProviderContext context, Action callNext)
{
foreach (var action in context.Actions.OfType<ControllerActionDescriptor>())
{
var extensionData = action.GetProperty<ApiDescriptionActionData>();
if (extensionData != null)
{
var httpMethods = GetHttpMethods(action);
foreach (var httpMethod in httpMethods)
{
context.Results.Add(CreateApiDescription(action, httpMethod, extensionData.GroupName));
}
}
}
callNext();
}
private ApiDescription CreateApiDescription(
ControllerActionDescriptor action,
string httpMethod,
string groupName)
{
var parsedTemplate = ParseTemplate(action);
var apiDescription = new ApiDescription()
{
ActionDescriptor = action,
GroupName = groupName,
HttpMethod = httpMethod,
RelativePath = GetRelativePath(parsedTemplate),
};
var templateParameters = parsedTemplate?.Parameters?.ToList() ?? new List<TemplatePart>();
GetParameters(apiDescription, action.Parameters, templateParameters);
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
// We only provide response info if we can figure out a type that is a user-data type.
// 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);
// 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))
{
// 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(
modelAccessor: null,
modelType: runtimeReturnType);
var formats = GetResponseFormats(
action,
responseMetadataAttributes,
declaredReturnType,
runtimeReturnType);
foreach (var format in formats)
{
apiDescription.SupportedResponseFormats.Add(format);
}
}
return apiDescription;
}
private void GetParameters(
ApiDescription apiDescription,
IList<ParameterDescriptor> parameterDescriptors,
IList<TemplatePart> templateParameters)
{
if (parameterDescriptors != null)
{
foreach (var parameter in parameterDescriptors)
{
// Process together parameters that appear on the path template and on the
// action descriptor and do not come from the body.
TemplatePart templateParameter = null;
if (parameter.BinderMetadata as IFormatterBinderMetadata == null)
{
templateParameter = templateParameters
.FirstOrDefault(p => p.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase));
if (templateParameter != null)
{
templateParameters.Remove(templateParameter);
}
}
apiDescription.ParameterDescriptions.Add(GetParameter(parameter, templateParameter));
}
}
if (templateParameters.Count > 0)
{
// Process parameters that only appear on the path template if any.
foreach (var templateParameter in templateParameters)
{
var parameterDescription = GetParameter(parameterDescriptor: null, templateParameter: templateParameter);
apiDescription.ParameterDescriptions.Add(parameterDescription);
}
}
}
private IEnumerable<string> GetHttpMethods(ControllerActionDescriptor action)
{
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
{
return action.ActionConstraints.OfType<HttpMethodConstraint>().SelectMany(c => c.HttpMethods);
}
else
{
return new string[] { null };
}
}
private RouteTemplate ParseTemplate(ControllerActionDescriptor action)
{
if (action.AttributeRouteInfo != null &&
action.AttributeRouteInfo.Template != null)
{
return TemplateParser.Parse(action.AttributeRouteInfo.Template);
}
return null;
}
private string GetRelativePath(RouteTemplate parsedTemplate)
{
if (parsedTemplate == null)
{
return null;
}
var segments = new List<string>();
foreach (var segment in parsedTemplate.Segments)
{
var currentSegment = "";
foreach (var part in segment.Parts)
{
if (part.IsLiteral)
{
currentSegment += part.Text;
}
else if (part.IsParameter)
{
currentSegment += "{" + part.Name + "}";
}
}
segments.Add(currentSegment);
}
return string.Join("/", segments);
}
private ApiParameterDescription GetParameter(
ParameterDescriptor parameterDescriptor,
TemplatePart templateParameter)
{
// This is a placeholder based on currently available functionality for parameters. See #886.
ApiParameterDescription parameterDescription = null;
if (templateParameter != null && parameterDescriptor == null)
{
// The parameter is part of the route template but not part of the ActionDescriptor.
// For now if a parameter is part of the template we will asume its value comes from the path.
// We will be more accurate when we implement #886.
parameterDescription = CreateParameterFromTemplate(templateParameter);
}
else if (templateParameter != null && parameterDescriptor != null)
{
// The parameter is part of the route template and part of the ActionDescriptor.
parameterDescription = CreateParameterFromTemplateAndParameterDescriptor(
templateParameter,
parameterDescriptor);
}
else if (templateParameter == null && parameterDescriptor != null)
{
// The parameter is part of the ActionDescriptor but is not part of the route template.
parameterDescription = CreateParameterFromParameterDescriptor(parameterDescriptor);
}
else
{
// We will never call this method with templateParameter == null && parameterDescriptor == null
Debug.Assert(parameterDescriptor != null);
}
if (parameterDescription.Type != null)
{
parameterDescription.ModelMetadata = _modelMetadataProvider.GetMetadataForType(
modelAccessor: null,
modelType: parameterDescription.Type);
}
return parameterDescription;
}
private static ApiParameterDescription CreateParameterFromParameterDescriptor(ParameterDescriptor parameter)
{
var resourceParameter = new ApiParameterDescription
{
Name = parameter.Name,
ParameterDescriptor = parameter,
Type = parameter.ParameterType,
};
if (parameter.BinderMetadata as IFormatterBinderMetadata != null)
{
resourceParameter.Source = ApiParameterSource.Body;
}
else
{
resourceParameter.Source = ApiParameterSource.Query;
}
return resourceParameter;
}
private ApiParameterDescription CreateParameterFromTemplateAndParameterDescriptor(
TemplatePart templateParameter,
ParameterDescriptor parameter)
{
var resourceParameter = new ApiParameterDescription
{
Source = ApiParameterSource.Path,
IsOptional = IsOptionalParameter(templateParameter),
Name = parameter.Name,
ParameterDescriptor = parameter,
Constraints = GetConstraints(_constraintResolver, templateParameter.InlineConstraints),
DefaultValue = templateParameter.DefaultValue,
Type = parameter.ParameterType,
};
return resourceParameter;
}
private static IEnumerable<IRouteConstraint> GetConstraints(
IInlineConstraintResolver constraintResolver,
IEnumerable<InlineConstraint> constraints)
{
return
constraints
.Select(c => constraintResolver.ResolveConstraint(c.Constraint))
.Where(c => c != null)
.ToArray();
}
private static bool IsOptionalParameter(TemplatePart templateParameter)
{
return templateParameter.IsOptional || templateParameter.DefaultValue != null;
}
private ApiParameterDescription CreateParameterFromTemplate(TemplatePart templateParameter)
{
return new ApiParameterDescription
{
Source = ApiParameterSource.Path,
IsOptional = IsOptionalParameter(templateParameter),
Name = templateParameter.Name,
ParameterDescriptor = null,
Constraints = GetConstraints(_constraintResolver, templateParameter.InlineConstraints),
DefaultValue = templateParameter.DefaultValue,
};
}
private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
ControllerActionDescriptor action,
IApiResponseMetadataProvider[] responseMetadataAttributes,
Type declaredType,
Type runtimeType)
{
var results = new List<ApiResponseFormat>();
// 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 (responseMetadataAttributes != null)
{
foreach (var metadataAttribute in responseMetadataAttributes)
{
metadataAttribute.SetContentTypes(contentTypes);
}
}
if (contentTypes.Count == 0)
{
contentTypes.Add(null);
}
var formatters = _formattersProvider.OutputFormatters;
foreach (var contentType in contentTypes)
{
foreach (var formatter in formatters)
{
var supportedTypes = formatter.GetSupportedContentTypes(declaredType, runtimeType, contentType);
if (supportedTypes != null)
{
foreach (var supportedType in supportedTypes)
{
results.Add(new ApiResponseFormat()
{
Formatter = formatter,
MediaType = supportedType,
});
}
}
}
}
return results;
}
private Type GetDeclaredReturnType(ControllerActionDescriptor action)
{
var declaredReturnType = action.MethodInfo.ReturnType;
if (declaredReturnType == typeof(void) ||
declaredReturnType == typeof(Task))
{
return typeof(void);
}
// Unwrap the type if it's a Task<T>. The Task (non-generic) case was already handled.
var unwrappedType = TypeHelper.GetTaskInnerTypeOrNull(declaredReturnType) ?? declaredReturnType;
// If the method is declared to return IActionResult or a derived class, that information
// isn't valuable to the formatter.
if (typeof(IActionResult).IsAssignableFrom(unwrappedType))
{
return null;
}
else
{
return unwrappedType;
}
}
private Type GetRuntimeReturnType(Type declaredReturnType, IApiResponseMetadataProvider[] metadataAttributes)
{
// 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.
//
// We've already excluded Task, void, and IActionResult at this point.
//
// If the action might return any object, then assume we don't know anything about it.
if (declaredReturnType == typeof(object))
{
return null;
}
return declaredReturnType;
}
private IApiResponseMetadataProvider[] GetResponseMetadataAttributes(ControllerActionDescriptor action)
{
if (action.FilterDescriptors == null)
{
return null;
}
// This technique for enumerating filters will intentionally ignore any filter that is an IFilterFactory
// for a filter that implements IApiResponseMetadataProvider.
//
// The workaround for that is to implement the metadata interface on the IFilterFactory.
return action.FilterDescriptors
.Select(fd => fd.Filter)
.OfType<IApiResponseMetadataProvider>()
.ToArray();
}
}
}