// 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 System.Linq; #if NETSTANDARD1_6 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; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ApiExplorer { /// /// Implements a provider of for actions represented /// by . /// public class DefaultApiDescriptionProvider : IApiDescriptionProvider { private readonly IList _inputFormatters; private readonly IList _outputFormatters; private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IInlineConstraintResolver _constraintResolver; /// /// Creates a new instance of . /// /// The accessor for . /// The used for resolving inline /// constraints. /// The . public DefaultApiDescriptionProvider( IOptions optionsAccessor, IInlineConstraintResolver constraintResolver, IModelMetadataProvider modelMetadataProvider) { _inputFormatters = optionsAccessor.Value.InputFormatters; _outputFormatters = optionsAccessor.Value.OutputFormatters; _constraintResolver = constraintResolver; _modelMetadataProvider = modelMetadataProvider; } /// public int Order { get { return -1000; } } /// public void OnProvidersExecuting(ApiDescriptionProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } 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)); } } } } public void OnProvidersExecuted(ApiDescriptionProviderContext context) { } 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(); var parameterContext = new ApiParameterContext(_modelMetadataProvider, action, templateParameters); foreach (var parameter in GetParameters(parameterContext)) { 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. // Void /Task object/IActionResult will result in no data. var declaredReturnType = GetDeclaredReturnType(action); var runtimeReturnType = GetRuntimeReturnType(declaredReturnType); var apiResponseTypes = GetApiResponseTypes(action, responseMetadataAttributes, runtimeReturnType); foreach (var apiResponseType in apiResponseTypes) { 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. foreach (var parameter in apiDescription.ParameterDescriptions.Where(p => p.Source == BindingSource.Body)) { var requestFormats = GetRequestFormats(action, requestMetadataAttributes, parameter.Type); foreach (var format in requestFormats) { apiDescription.SupportedRequestFormats.Add(format); } } return apiDescription; } private IList GetParameters(ApiParameterContext context) { // First, get parameters from the model-binding/parameter-binding side of the world. if (context.ActionDescriptor.Parameters != null) { foreach (var actionParameter in context.ActionDescriptor.Parameters) { var visitor = new PseudoModelBindingVisitor(context, actionParameter); var metadata = _modelMetadataProvider.GetMetadataForType(actionParameter.ParameterType); var bindingContext = ApiParameterDescriptionContext.GetContext( metadata, actionParameter.BindingInfo, propertyName: actionParameter.Name); visitor.WalkParameter(bindingContext); } } if (context.ActionDescriptor.BoundProperties != null) { foreach (var actionParameter in context.ActionDescriptor.BoundProperties) { var visitor = new PseudoModelBindingVisitor(context, actionParameter); var modelMetadata = context.MetadataProvider.GetMetadataForProperty( containerType: context.ActionDescriptor.ControllerTypeInfo.AsType(), propertyName: actionParameter.Name); var bindingContext = ApiParameterDescriptionContext.GetContext( modelMetadata, actionParameter.BindingInfo, propertyName: actionParameter.Name); visitor.WalkParameter(bindingContext); } } for (var i = context.Results.Count - 1; i >= 0; i--) { // Remove any 'hidden' parameters. These are things that can't come from user input, // so they aren't worth showing. if (!context.Results[i].Source.IsFromRequest) { context.Results.RemoveAt(i); } } // Next, we want to join up any route parameters with those discovered from the action's parameters. var routeParameters = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var routeParameter in context.RouteParameters) { routeParameters.Add(routeParameter.Name, CreateRouteInfo(routeParameter)); } foreach (var parameter in context.Results) { if (parameter.Source == BindingSource.Path || parameter.Source == BindingSource.ModelBinding || parameter.Source == BindingSource.Custom) { ApiParameterRouteInfo routeInfo; if (routeParameters.TryGetValue(parameter.Name, out routeInfo)) { parameter.RouteInfo = routeInfo; routeParameters.Remove(parameter.Name); if (parameter.Source == BindingSource.ModelBinding && !parameter.RouteInfo.IsOptional) { // If we didn't see any information about the parameter, but we have // a route parameter that matches, let's switch it to path. parameter.Source = BindingSource.Path; } } } } // Lastly, create a parameter representation for each route parameter that did not find // a partner. foreach (var routeParameter in routeParameters) { context.Results.Add(new ApiParameterDescription() { Name = routeParameter.Key, RouteInfo = routeParameter.Value, Source = BindingSource.Path, }); } return context.Results; } private ApiParameterRouteInfo CreateRouteInfo(TemplatePart routeParameter) { var constraints = new List(); if (routeParameter.InlineConstraints != null) { foreach (var constraint in routeParameter.InlineConstraints) { constraints.Add(_constraintResolver.ResolveConstraint(constraint.Constraint)); } } return new ApiParameterRouteInfo() { Constraints = constraints, DefaultValue = routeParameter.DefaultValue, IsOptional = routeParameter.IsOptional || routeParameter.DefaultValue != null, }; } 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 = string.Empty; 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 IReadOnlyList GetRequestFormats( ControllerActionDescriptor action, IApiRequestMetadataProvider[] requestMetadataAttributes, Type type) { 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 MediaTypeCollection(); if (requestMetadataAttributes != null) { foreach (var metadataAttribute in requestMetadataAttributes) { metadataAttribute.SetContentTypes(contentTypes); } } if (contentTypes.Count == 0) { contentTypes.Add((string)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 GetApiResponseTypes( ControllerActionDescriptor action, IApiResponseMetadataProvider[] responseMetadataAttributes, Type type) { var results = new List(); // Build list of all possible return types (and status codes) for an action. var objectTypes = new Dictionary(); // 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(); if (responseMetadataAttributes != null) { foreach (var metadataAttribute in responseMetadataAttributes) { metadataAttribute.SetContentTypes(contentTypes); if (metadataAttribute.Type != null) { objectTypes[metadataAttribute.StatusCode] = metadataAttribute.Type; } } } // Set the default status only when no status has already been set explicitly if (objectTypes.Count == 0 && type != null && type != typeof(void)) { objectTypes[StatusCodes.Status200OK] = type; } if (contentTypes.Count == 0) { contentTypes.Add((string)null); } var responseTypeMetadataProviders = _outputFormatters.OfType(); foreach (var objectType in objectTypes) { if (objectType.Value == typeof(void)) { results.Add(new ApiResponseType() { StatusCode = objectType.Key, Type = objectType.Value }); 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) { continue; } foreach (var formatterSupportedContentType in formatterSupportedContentTypes) { apiResponseType.ApiResponseFormats.Add(new ApiResponseFormat() { Formatter = (IOutputFormatter)responseTypeMetadataProvider, MediaType = formatterSupportedContentType, }); } } } results.Add(apiResponseType); } 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 = 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 static Type GetTaskInnerTypeOrNull(Type type) { var genericType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(Task<>)); return genericType?.GenericTypeArguments[0]; } private Type GetRuntimeReturnType(Type declaredReturnType) { // 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 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() .ToArray(); } 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 // while searching 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(); } private class ApiParameterContext { public ApiParameterContext( IModelMetadataProvider metadataProvider, ControllerActionDescriptor actionDescriptor, IReadOnlyList routeParameters) { MetadataProvider = metadataProvider; ActionDescriptor = actionDescriptor; RouteParameters = routeParameters; Results = new List(); } public ControllerActionDescriptor ActionDescriptor { get; } public IModelMetadataProvider MetadataProvider { get; } public IList Results { get; } public IReadOnlyList RouteParameters { get; } } private class ApiParameterDescriptionContext { public ModelMetadata ModelMetadata { get; set; } public string BinderModelName { get; set; } public BindingSource BindingSource { get; set; } public string PropertyName { get; set; } public static ApiParameterDescriptionContext GetContext( ModelMetadata metadata, BindingInfo bindingInfo, string propertyName) { // BindingMetadata can be null if the metadata represents properties. return new ApiParameterDescriptionContext { ModelMetadata = metadata, BinderModelName = bindingInfo?.BinderModelName ?? metadata.BinderModelName, BindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource, PropertyName = propertyName ?? metadata.PropertyName }; } } private class PseudoModelBindingVisitor { public PseudoModelBindingVisitor(ApiParameterContext context, ParameterDescriptor parameter) { Context = context; Parameter = parameter; Visited = new HashSet(); } public ApiParameterContext Context { get; } public ParameterDescriptor Parameter { get; } // Avoid infinite recursion by tracking properties. private HashSet Visited { get; } public void WalkParameter(ApiParameterDescriptionContext context) { // Attempt to find a binding source for the parameter // // The default is ModelBinding (aka all default value providers) var source = BindingSource.ModelBinding; Visit(context, source, containerName: string.Empty); } private void Visit( ApiParameterDescriptionContext bindingContext, BindingSource ambientSource, string containerName) { var source = bindingContext.BindingSource; if (source != null && source.IsGreedy) { // We have a definite answer for this model. This is a greedy source like // [FromBody] so there's no need to consider properties. Context.Results.Add(CreateResult(bindingContext, source, containerName)); return; } var modelMetadata = bindingContext.ModelMetadata; // For any property which is a leaf node, we don't want to keep traversing: // // 1) Collections - while it's possible to have binder attributes on the inside of a collection, // it hardly seems useful, and would result in some very wierd binding. // // 2) Simple Types - These are generally part of the .net framework - primitives, or types which have a // type converter from string. // // 3) Types with no properties. Obviously nothing to explore there. // if (modelMetadata.IsEnumerableType || !modelMetadata.IsComplexType || !modelMetadata.Properties.Any()) { Context.Results.Add(CreateResult(bindingContext, source ?? ambientSource, containerName)); return; } // This will come from composite model binding - so investigate what's going on with each property. // // Ex: // // public IActionResult PlaceOrder(OrderDTO order) {...} // // public class OrderDTO // { // public int AccountId { get; set; } // // [FromBody] // public Order { get; set; } // } // // This should result in two parameters: // // AccountId - source: Any // Order - source: Body // // We don't want to append the **parameter** name when building a model name. var newContainerName = containerName; if (modelMetadata.ContainerType != null) { newContainerName = GetName(containerName, bindingContext); } foreach (var propertyMetadata in modelMetadata.Properties) { var key = new PropertyKey(propertyMetadata, source); var propertyContext = ApiParameterDescriptionContext.GetContext( propertyMetadata, bindingInfo: null, propertyName: null); if (Visited.Add(key)) { Visit(propertyContext, source ?? ambientSource, newContainerName); } else { // This is cycle, so just add a result rather than traversing. Context.Results.Add(CreateResult(propertyContext, source ?? ambientSource, newContainerName)); } } } private ApiParameterDescription CreateResult( ApiParameterDescriptionContext bindingContext, BindingSource source, string containerName) { return new ApiParameterDescription() { ModelMetadata = bindingContext.ModelMetadata, Name = GetName(containerName, bindingContext), Source = source, Type = bindingContext.ModelMetadata.ModelType, }; } private static string GetName(string containerName, ApiParameterDescriptionContext metadata) { if (!string.IsNullOrEmpty(metadata.BinderModelName)) { // Name was explicitly provided return metadata.BinderModelName; } else { return ModelNames.CreatePropertyModelName(containerName, metadata.PropertyName); } } private struct PropertyKey { public readonly Type ContainerType; public readonly string PropertyName; public readonly BindingSource Source; public PropertyKey(ModelMetadata metadata, BindingSource source) { ContainerType = metadata.ContainerType; PropertyName = metadata.PropertyName; Source = source; } } private class PropertyKeyEqualityComparer : IEqualityComparer { public bool Equals(PropertyKey x, PropertyKey y) { return x.ContainerType == y.ContainerType && x.PropertyName == y.PropertyName && x.Source == y.Source; } public int GetHashCode(PropertyKey obj) { return obj.ContainerType.GetHashCode() ^ obj.PropertyName.GetHashCode() ^ obj.Source.GetHashCode(); } } } } }