// 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 { /// /// Implements a provider of for actions represented /// by . /// public class DefaultApiDescriptionProvider : INestedProvider { private readonly IOutputFormattersProvider _formattersProvider; private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IInlineConstraintResolver _constraintResolver; /// /// Creates a new instance of . /// /// The . /// The . public DefaultApiDescriptionProvider( IOutputFormattersProvider formattersProvider, IInlineConstraintResolver constraintResolver, IModelMetadataProvider modelMetadataProvider) { _formattersProvider = formattersProvider; _modelMetadataProvider = modelMetadataProvider; _constraintResolver = constraintResolver; } /// public int Order { get { return DefaultOrder.DefaultFrameworkSortOrder; } } /// public void Invoke(ApiDescriptionProviderContext context, Action callNext) { foreach (var action in context.Actions.OfType()) { var extensionData = action.GetProperty(); 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(); 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 parameterDescriptors, IList 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 GetHttpMethods(ControllerActionDescriptor action) { if (action.ActionConstraints != null && action.ActionConstraints.Count > 0) { return action.ActionConstraints.OfType().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(); 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 GetConstraints( IInlineConstraintResolver constraintResolver, IEnumerable 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 GetResponseFormats( ControllerActionDescriptor action, IApiResponseMetadataProvider[] responseMetadataAttributes, Type declaredType, Type runtimeType) { var results = new List(); // 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(); 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. 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() .ToArray(); } } }