// 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; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Infrastructure; 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 MvcOptions _mvcOptions; private readonly IActionResultTypeMapper _mapper; private readonly ApiResponseTypeProvider _responseTypeProvider; private readonly RouteOptions _routeOptions; private readonly IInlineConstraintResolver _constraintResolver; private readonly IModelMetadataProvider _modelMetadataProvider; /// /// Creates a new instance of . /// /// The accessor for . /// The used for resolving inline /// constraints. /// The . [Obsolete("This constructor is obsolete and will be removed in a future release.")] public DefaultApiDescriptionProvider( IOptions optionsAccessor, IInlineConstraintResolver constraintResolver, IModelMetadataProvider modelMetadataProvider) : this(optionsAccessor, constraintResolver, modelMetadataProvider, null) { } /// /// Creates a new instance of . /// /// The accessor for . /// The used for resolving inline /// constraints. /// The . /// The . [Obsolete("This constructor is obsolete and will be removed in a future release.")] public DefaultApiDescriptionProvider( IOptions optionsAccessor, IInlineConstraintResolver constraintResolver, IModelMetadataProvider modelMetadataProvider, IActionResultTypeMapper mapper) { _mvcOptions = optionsAccessor.Value; _constraintResolver = constraintResolver; _modelMetadataProvider = modelMetadataProvider; _mapper = mapper; _responseTypeProvider = new ApiResponseTypeProvider(modelMetadataProvider, mapper, _mvcOptions); } /// /// Creates a new instance of . /// /// The accessor for . /// The used for resolving inline /// constraints. /// The . /// The . /// The accessor for . public DefaultApiDescriptionProvider( IOptions optionsAccessor, IInlineConstraintResolver constraintResolver, IModelMetadataProvider modelMetadataProvider, IActionResultTypeMapper mapper, IOptions routeOptions) { _mvcOptions = optionsAccessor.Value; _constraintResolver = constraintResolver; _modelMetadataProvider = modelMetadataProvider; _mapper = mapper; _responseTypeProvider = new ApiResponseTypeProvider(modelMetadataProvider, mapper, _mvcOptions); _routeOptions = routeOptions.Value; } /// public int Order => -1000; /// public void OnProvidersExecuting(ApiDescriptionProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } foreach (var action in context.Actions.OfType()) { if (action.AttributeRouteInfo != null && action.AttributeRouteInfo.SuppressPathMatching) { continue; } 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 apiResponseTypes = _responseTypeProvider.GetApiResponseTypes(action); 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. if (apiDescription.ParameterDescriptions.Count > 0) { var contentTypes = GetDeclaredContentTypes(requestMetadataAttributes); foreach (var parameter in apiDescription.ParameterDescriptions) { if (parameter.Source == BindingSource.Body) { // For request body bound parameters, determine the content types supported // by input formatters. var requestFormats = GetSupportedFormats(contentTypes, parameter.Type); foreach (var format in requestFormats) { apiDescription.SupportedRequestFormats.Add(format); } } else if (parameter.Source == BindingSource.FormFile) { // Add all declared media types since FormFiles do not get processed by formatters. foreach (var contentType in contentTypes) { apiDescription.SupportedRequestFormats.Add(new ApiRequestFormat { MediaType = contentType, }); } } } } 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); ModelMetadata metadata; if (_mvcOptions.AllowValidatingTopLevelNodes && actionParameter is ControllerParameterDescriptor controllerParameterDescriptor && _modelMetadataProvider is ModelMetadataProvider provider) { // The default model metadata provider derives from ModelMetadataProvider // and can therefore supply information about attributes applied to parameters. metadata = provider.GetMetadataForParameter(controllerParameterDescriptor.ParameterInfo); } else { // For backward compatibility, if there's a custom model metadata provider that // only implements the older IModelMetadataProvider interface, access the more // limited metadata information it supplies. In this scenario, validation attributes // are not supported on parameters. 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. // This will result us in creating a parameter representation for each route parameter that does not // have a mapping parameter or bound property. ProcessRouteParameters(context); // Set IsRequired=true ProcessIsRequired(context); // Set DefaultValue ProcessParameterDefaultValue(context); return context.Results; } private void ProcessRouteParameters(ApiParameterContext context) { 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) { if (routeParameters.TryGetValue(parameter.Name, out var 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, }); } } internal static void ProcessIsRequired(ApiParameterContext context) { foreach (var parameter in context.Results) { if (parameter.Source == BindingSource.Body) { parameter.IsRequired = true; } if (parameter.ModelMetadata != null && parameter.ModelMetadata.IsBindingRequired) { parameter.IsRequired = true; } if (parameter.Source == BindingSource.Path && parameter.RouteInfo != null && !parameter.RouteInfo.IsOptional) { parameter.IsRequired = true; } } } internal static void ProcessParameterDefaultValue(ApiParameterContext context) { foreach (var parameter in context.Results) { if (parameter.Source == BindingSource.Path) { parameter.DefaultValue = parameter.RouteInfo?.DefaultValue; } else { if (parameter.ParameterDescriptor is ControllerParameterDescriptor controllerParameter && ParameterDefaultValues.TryGetDeclaredParameterDefaultValue(controllerParameter.ParameterInfo, out var defaultValue)) { parameter.DefaultValue = defaultValue; } } } } 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?.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 += _routeOptions.LowercaseUrls ? part.Text.ToLowerInvariant() : part.Text; } else if (part.IsParameter) { currentSegment += "{" + part.Name + "}"; } } segments.Add(currentSegment); } return string.Join("/", segments); } private IReadOnlyList GetSupportedFormats(MediaTypeCollection contentTypes, Type type) { if (contentTypes.Count == 0) { contentTypes = new MediaTypeCollection { (string)null, }; } var results = new List(); foreach (var contentType in contentTypes) { foreach (var formatter in _mvcOptions.InputFormatters) { if (formatter is IApiRequestFormatMetadataProvider requestFormatMetadataProvider) { 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 static MediaTypeCollection GetDeclaredContentTypes(IApiRequestMetadataProvider[] requestMetadataAttributes) { // 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); } } return contentTypes; } 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 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, BindingSource = bindingInfo?.BindingSource, PropertyName = propertyName ?? metadata.Name, }; } } private class PseudoModelBindingVisitor { public PseudoModelBindingVisitor(ApiParameterContext context, ParameterDescriptor parameter) { Context = context; Parameter = parameter; Visited = new HashSet(new PropertyKeyEqualityComparer()); } 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 weird 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.Count == 0) { 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); } for (var i = 0; i < modelMetadata.Properties.Count; i++) { var propertyMetadata = modelMetadata.Properties[i]; var key = new PropertyKey(propertyMetadata, source); var bindingInfo = BindingInfo.GetBindingInfo(Enumerable.Empty(), propertyMetadata); var propertyContext = ApiParameterDescriptionContext.GetContext( propertyMetadata, bindingInfo: bindingInfo, 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, ParameterDescriptor = Parameter, }; } 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 readonly 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) { var hashCodeCombiner = HashCodeCombiner.Start(); hashCodeCombiner.Add(obj.ContainerType); hashCodeCombiner.Add(obj.PropertyName); hashCodeCombiner.Add(obj.Source); return hashCodeCombiner.CombinedHash; } } } } }