// Copyright (c) Microsoft Open Technologies, Inc. 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.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { /// /// Represents an that delegates to one of a collection of /// instances. /// /// /// If no binder is available and the allows it, /// this class tries to find a binder using an empty prefix. /// public class CompositeModelBinder : ICompositeModelBinder { /// /// Initializes a new instance of the CompositeModelBinder class. /// /// A collection of instances. public CompositeModelBinder([NotNull] IEnumerable modelBinders) { ModelBinders = new List(modelBinders); } /// public IReadOnlyList ModelBinders { get; } public virtual async Task BindModelAsync([NotNull] ModelBindingContext bindingContext) { var newBindingContext = CreateNewBindingContext(bindingContext, bindingContext.ModelName); var modelBindingResult = await TryBind(newBindingContext); if (modelBindingResult == null && bindingContext.FallbackToEmptyPrefix && !string.IsNullOrEmpty(bindingContext.ModelName)) { // Fall back to empty prefix. newBindingContext = CreateNewBindingContext(bindingContext, modelName: string.Empty); modelBindingResult = await TryBind(newBindingContext); } if (modelBindingResult == null) { return null; // something went wrong } bindingContext.OperationBindingContext.BodyBindingState = newBindingContext.OperationBindingContext.BodyBindingState; if (modelBindingResult.IsModelSet) { // Update the model state key if we are bound using an empty prefix and it is a complex type. // This is needed as validation uses the model state key to log errors. The client validation expects // the errors with property names rather than the full name. if (newBindingContext.ModelMetadata.IsComplexType && string.IsNullOrEmpty(modelBindingResult.Key)) { // For non-complex types, if we fell back to the empty prefix, we should still be using the name // of the parameter/property. Complex types have their own property names which acts as model // state keys and do not need special treatment. // For example : // // public class Model // { // public int SimpleType { get; set; } // } // public void Action(int id, Model model) // { // } // // In this case, for the model parameter the key would be SimpleType instead of model.SimpleType. // (i.e here the prefix for the model key is empty). // For the id parameter the key would be id. return modelBindingResult; } } // Fall through to update the ModelBindingResult's key. return new ModelBindingResult( modelBindingResult.Model, bindingContext.ModelName, modelBindingResult.IsModelSet); } private async Task TryBind(ModelBindingContext bindingContext) { RuntimeHelpers.EnsureSufficientExecutionStack(); foreach (var binder in ModelBinders) { var result = await binder.BindModelAsync(bindingContext); if (result != null) { // Use returned ModelBindingResult if it either indicates the model was set or is related to a // ModelState entry. The second condition is necessary because the ModelState entry would never be // validated if caller fell back to the empty prefix, leading to an possibly-incorrect !IsValid. // // In most (hopefully all) cases, the ModelState entry exists because some binders add errors // before returning a result with !IsModelSet. Those binders often cannot run twice anyhow. if (result.IsModelSet || bindingContext.ModelState.ContainsKey(bindingContext.ModelName)) { return result; } // Current binder should have been able to bind value but found nothing. Exit loop in a way that // tells caller to fall back to the empty prefix, if appropriate. Do not return result because it // means only "other binders are not applicable". break; } } // Either we couldn't find a binder, or the binder couldn't bind. Distinction is not important. return null; } private static ModelBindingContext CreateNewBindingContext(ModelBindingContext oldBindingContext, string modelName) { var newBindingContext = new ModelBindingContext { Model = oldBindingContext.Model, ModelMetadata = oldBindingContext.ModelMetadata, ModelName = modelName, ModelState = oldBindingContext.ModelState, ValueProvider = oldBindingContext.ValueProvider, OperationBindingContext = oldBindingContext.OperationBindingContext, PropertyFilter = oldBindingContext.PropertyFilter, BinderModelName = oldBindingContext.BinderModelName, BindingSource = oldBindingContext.BindingSource, BinderType = oldBindingContext.BinderType, }; newBindingContext.OperationBindingContext.BodyBindingState = GetBodyBindingState(oldBindingContext); // If the property has a specified data binding sources, we need to filter the set of value providers // to just those that match. We can skip filtering when IsGreedy == true, because that can't use // value providers. // // We also want to base this filtering on the - top-level value provider in case the data source // on this property doesn't intersect with the ambient data source. // // Ex: // // public class Person // { // [FromQuery] // public int Id { get; set; } // } // // public IActionResult UpdatePerson([FromForm] Person person) { } // // In this example, [FromQuery] overrides the ambient data source (form). var bindingSource = oldBindingContext.BindingSource; if (bindingSource != null && !bindingSource.IsGreedy) { var valueProvider = oldBindingContext.OperationBindingContext.ValueProvider as IBindingSourceValueProvider; if (valueProvider != null) { newBindingContext.ValueProvider = valueProvider.Filter(bindingSource); } } return newBindingContext; } private static BodyBindingState GetBodyBindingState(ModelBindingContext oldBindingContext) { var bindingSource = oldBindingContext.BindingSource; var willReadBodyWithFormatter = bindingSource == BindingSource.Body; var willReadBodyAsFormData = bindingSource == BindingSource.Form; var currentModelNeedsToReadBody = willReadBodyWithFormatter || willReadBodyAsFormData; var oldState = oldBindingContext.OperationBindingContext.BodyBindingState; // We need to throw if there are multiple models which can cause body to be read multiple times. // Reading form data multiple times is ok since we cache form data. For the models marked to read using // formatters, multiple reads are not allowed. if (oldState == BodyBindingState.FormatterBased && currentModelNeedsToReadBody || oldState == BodyBindingState.FormBased && willReadBodyWithFormatter) { throw new InvalidOperationException(Resources.MultipleBodyParametersOrPropertiesAreNotAllowed); } var state = oldBindingContext.OperationBindingContext.BodyBindingState; if (willReadBodyWithFormatter) { state = BodyBindingState.FormatterBased; } else if (willReadBodyAsFormData && oldState != BodyBindingState.FormatterBased) { // Only update the model binding state if we have not discovered formatter based state already. state = BodyBindingState.FormBased; } return state; } } }