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 = ApiParameterDescriptionContext.GetContext( metadata, actionParameter.BindingInfo, propertyName: actionParameter.Name); visitor.WalkParameter(bindingContext); }
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)); } } }
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); }
/// <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); } }