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);

                ModelMetadata metadata;
                if (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 = new ApiParameterDescriptionContext(
                    metadata,
                    actionParameter.BindingInfo,
                    propertyName: actionParameter.Name);
                visitor.WalkParameter(bindingContext);
            }
            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 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));
     }
 }
예제 #4
0
 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,
     });
 }
예제 #5
0
            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;

                if (!Visit(context, source, containerName: string.Empty))
                {
                    // If we get here, then it means we didn't find a match for any of the model. This means that it's
                    // likely 'model-bound' in the traditional MVC sense (formdata + query string + route data) and
                    // doesn't use any IBinderMetadata.
                    //
                    // Add a single 'default' parameter description for the model.
                    Context.Results.Add(CreateResult(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 <object>(), 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));
                    }
                }
            }
예제 #7
0
        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);
        }
예제 #8
0
            private static string GetName(string containerName, ApiParameterDescriptionContext metadata)
            {
                var propertyName = !string.IsNullOrEmpty(metadata.BinderModelName) ? metadata.BinderModelName : metadata.PropertyName;

                return(ModelNames.CreatePropertyModelName(containerName, propertyName));
            }
예제 #9
0
            /// <summary>
            /// Visits a node in a model, and attempts to create <see cref="ApiParameterDescription"/> for any
            /// model properties where we can definitely compute an answer.
            /// </summary>
            /// <param name="modelMetadata">The metadata for the model.</param>
            /// <param name="ambientSource">The <see cref="BindingSource"/> from the ambient context.</param>
            /// <param name="containerName">The current name prefix (to prepend to property names).</param>
            /// <returns>
            /// <c>true</c> if the set of <see cref="ApiParameterDescription"/> objects were created for the model.
            /// <c>false</c> if no <see cref="ApiParameterDescription"/> objects were created for the model.
            /// </returns>
            /// <remarks>
            /// Its the reponsibility of this method to create a parameter description for ALL of the current model
            /// or NONE of it. If a parameter description is created for ANY sub-properties of the model, then a parameter
            /// description will be created for ALL of them.
            /// </remarks>
            private bool 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(true);
                }

                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.IsCollectionType ||
                    !modelMetadata.IsComplexType ||
                    !modelMetadata.Properties.Any())
                {
                    if (source == null || source == ambientSource)
                    {
                        // If it's a leaf node, and we have no new source then we don't know how to bind this.
                        // Return without creating any parameters, so that this can be included in the parent model.
                        return(false);
                    }
                    else
                    {
                        // We found a new source, and this model has no properties. This is probabaly
                        // a simple type with an attribute like [FromQuery].
                        Context.Results.Add(CreateResult(bindingContext, source, containerName));
                        return(true);
                    }
                }

                // This will come from composite model binding - so investigate what's going on with each property.
                //
                // Basically once we find something that we know how to bind, we want to treat all properties at that
                // level (and higher levels) as separate parameters.
                //
                // 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
                //

                var unboundProperties = new HashSet <ApiParameterDescriptionContext>();

                // 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))
                    {
                        if (!Visit(propertyContext, source ?? ambientSource, newContainerName))
                        {
                            unboundProperties.Add(propertyContext);
                        }
                    }
                    else
                    {
                        unboundProperties.Add(propertyContext);
                    }
                }

                if (unboundProperties.Count == modelMetadata.Properties.Count)
                {
                    if (source == null || source == ambientSource)
                    {
                        // No properties were bound and we didn't find a new source, let the caller handle it.
                        return(false);
                    }
                    else
                    {
                        // We found a new source, and didn't create a result for any of the properties yet,
                        // so create a result for the current object.
                        Context.Results.Add(CreateResult(bindingContext, source, containerName));
                        return(true);
                    }
                }
                else
                {
                    // This model was only partially bound, so create a result for all the other properties
                    foreach (var property in unboundProperties)
                    {
                        // Create a 'default' description for each property
                        Context.Results.Add(CreateResult(property, source ?? ambientSource, newContainerName));
                    }

                    return(true);
                }
            }