744 lines
29 KiB
C#
744 lines
29 KiB
C#
// 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 System.Reflection;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Implements a provider of <see cref="ApiDescription"/> for actions represented
|
|
/// by <see cref="ControllerActionDescriptor"/>.
|
|
/// </summary>
|
|
public class DefaultApiDescriptionProvider : IApiDescriptionProvider
|
|
{
|
|
private readonly IList<IInputFormatter> _inputFormatters;
|
|
private readonly IList<IOutputFormatter> _outputFormatters;
|
|
private readonly IModelMetadataProvider _modelMetadataProvider;
|
|
private readonly IInlineConstraintResolver _constraintResolver;
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of <see cref="DefaultApiDescriptionProvider"/>.
|
|
/// </summary>
|
|
/// <param name="optionsAccessor">The accessor for <see cref="MvcOptions"/>.</param>
|
|
/// <param name="constraintResolver">The <see cref="IInlineConstraintResolver"/> used for resolving inline
|
|
/// constraints.</param>
|
|
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
|
public DefaultApiDescriptionProvider(
|
|
IOptions<MvcOptions> optionsAccessor,
|
|
IInlineConstraintResolver constraintResolver,
|
|
IModelMetadataProvider modelMetadataProvider)
|
|
{
|
|
_inputFormatters = optionsAccessor.Value.InputFormatters;
|
|
_outputFormatters = optionsAccessor.Value.OutputFormatters;
|
|
_constraintResolver = constraintResolver;
|
|
_modelMetadataProvider = modelMetadataProvider;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public int Order
|
|
{
|
|
get { return -1000; }
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
foreach (var action in context.Actions.OfType<ControllerActionDescriptor>())
|
|
{
|
|
if (action.AttributeRouteInfo != null && action.AttributeRouteInfo.SuppressPathMatching)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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<TemplatePart>();
|
|
|
|
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<ApiParameterDescription> 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<string, ApiParameterRouteInfo>(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<IRouteConstraint>();
|
|
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<string> GetHttpMethods(ControllerActionDescriptor action)
|
|
{
|
|
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
|
|
{
|
|
return action.ActionConstraints.OfType<HttpMethodActionConstraint>().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 = 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<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 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<ApiResponseType> GetApiResponseTypes(
|
|
ControllerActionDescriptor action,
|
|
IApiResponseMetadataProvider[] responseMetadataAttributes,
|
|
Type type)
|
|
{
|
|
var results = new List<ApiResponseType>();
|
|
|
|
// Build list of all possible return types (and status codes) for an action.
|
|
var objectTypes = new Dictionary<int, Type>();
|
|
|
|
// 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)
|
|
{
|
|
objectTypes[StatusCodes.Status200OK] = type;
|
|
}
|
|
|
|
if (contentTypes.Count == 0)
|
|
{
|
|
contentTypes.Add((string)null);
|
|
}
|
|
|
|
var responseTypeMetadataProviders = _outputFormatters.OfType<IApiResponseTypeMetadataProvider>();
|
|
|
|
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<T>. 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<IApiRequestMetadataProvider>()
|
|
.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<IApiResponseMetadataProvider>()
|
|
.ToArray();
|
|
}
|
|
|
|
private class ApiParameterContext
|
|
{
|
|
public ApiParameterContext(
|
|
IModelMetadataProvider metadataProvider,
|
|
ControllerActionDescriptor actionDescriptor,
|
|
IReadOnlyList<TemplatePart> routeParameters)
|
|
{
|
|
MetadataProvider = metadataProvider;
|
|
ActionDescriptor = actionDescriptor;
|
|
RouteParameters = routeParameters;
|
|
|
|
Results = new List<ApiParameterDescription>();
|
|
}
|
|
|
|
public ControllerActionDescriptor ActionDescriptor { get; }
|
|
|
|
public IModelMetadataProvider MetadataProvider { get; }
|
|
|
|
public IList<ApiParameterDescription> Results { get; }
|
|
|
|
public IReadOnlyList<TemplatePart> 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<PropertyKey>();
|
|
}
|
|
|
|
public ApiParameterContext Context { get; }
|
|
|
|
public ParameterDescriptor Parameter { get; }
|
|
|
|
// Avoid infinite recursion by tracking properties.
|
|
private HashSet<PropertyKey> 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.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 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,
|
|
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 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<PropertyKey>
|
|
{
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |