// 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.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { /// /// implementation for binding complex values. /// public class MutableObjectModelBinder : IModelBinder { private static readonly MethodInfo CallPropertyAddRangeOpenGenericMethod = typeof(MutableObjectModelBinder).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyAddRange)); /// public virtual async Task BindModelAsync([NotNull] ModelBindingContext bindingContext) { ModelBindingHelper.ValidateBindingContext(bindingContext); if (!CanBindType(bindingContext.ModelMetadata)) { return null; } var mutableObjectBinderContext = new MutableObjectBinderContext() { ModelBindingContext = bindingContext, PropertyMetadata = GetMetadataForProperties(bindingContext).ToArray(), }; if (!(await CanCreateModel(mutableObjectBinderContext))) { return null; } // Create model first (if necessary) to avoid reporting errors about properties when activation fails. var model = GetModel(bindingContext); var results = await BindPropertiesAsync(bindingContext, mutableObjectBinderContext.PropertyMetadata); var validationNode = new ModelValidationNode( bindingContext.ModelName, bindingContext.ModelMetadata, model); // Post-processing e.g. property setters and hooking up validation. bindingContext.Model = model; ProcessResults(bindingContext, results, validationNode); return new ModelBindingResult( model, bindingContext.ModelName, isModelSet: true, validationNode: validationNode); } /// /// Gets an indication whether a property with the given can be updated. /// /// for the property of interest. /// true if the property can be updated; false otherwise. /// Should return true only for properties can update. protected virtual bool CanUpdateProperty([NotNull] ModelMetadata propertyMetadata) { return CanUpdatePropertyInternal(propertyMetadata); } internal async Task CanCreateModel(MutableObjectBinderContext context) { var bindingContext = context.ModelBindingContext; var isTopLevelObject = bindingContext.IsTopLevelObject; // If we get here the model is a complex object which was not directly bound by any previous model binder, // so we want to decide if we want to continue binding. This is important to get right to avoid infinite // recursion. // // First, we want to make sure this object is allowed to come from a value provider source as this binder // will always include value provider data. For instance if the model is marked with [FromBody], then we // can just skip it. A greedy source cannot be a value provider. // // If the model isn't marked with ANY binding source, then we assume it's OK also. // // We skip this check if it is a top level object because we want to always evaluate // the creation of top level object (this is also required for ModelBinderAttribute to work.) var bindingSource = bindingContext.BindingSource; if (!isTopLevelObject && bindingSource != null && bindingSource.IsGreedy) { return false; } // Create the object if: // 1. It is a top level model and no later fallback (to empty prefix) will occur. if (isTopLevelObject && !bindingContext.IsFirstChanceBinding) { return true; } // 2. If it is top level object and there are no properties to bind if (isTopLevelObject && context.PropertyMetadata != null && context.PropertyMetadata.Count == 0) { return true; } // 3. Any of the model properties can be bound using a value provider. if (await CanValueBindAnyModelProperties(context)) { return true; } return false; } private async Task CanValueBindAnyModelProperties(MutableObjectBinderContext context) { // If there are no properties on the model, there is nothing to bind. We are here means this is not a top // level object. So we return false. if (context.PropertyMetadata == null || context.PropertyMetadata.Count == 0) { return false; } // We want to check to see if any of the properties of the model can be bound using the value providers, // because that's all that MutableObjectModelBinder can handle. // // However, because a property might specify a custom binding source ([FromForm]), it's not correct // for us to just try bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName), // because that may include ALL value providers - that would lead us to mistakenly create the model // when the data is coming from a source we should use (ex: value found in query string, but the // model has [FromForm]). // // To do this we need to enumerate the properties, and see which of them provide a binding source // through metadata, then we decide what to do. // // If a property has a binding source, and it's a greedy source, then it's not // allowed to come from a value provider, so we skip it. // // If a property has a binding source, and it's a non-greedy source, then we'll filter the // the value providers to just that source, and see if we can find a matching prefix // (see CanBindValue). // // If a property does not have a binding source, then it's fair game for any value provider. // // If any property meets the above conditions and has a value from valueproviders, then we'll // create the model and try to bind it. OR if ALL properties of the model have a greedy source, // then we go ahead and create it. // var isAnyPropertyEnabledForValueProviderBasedBinding = false; foreach (var propertyMetadata in context.PropertyMetadata) { // This check will skip properties which are marked explicitly using a non value binder. var bindingSource = propertyMetadata.BindingSource; if (bindingSource == null || !bindingSource.IsGreedy) { isAnyPropertyEnabledForValueProviderBasedBinding = true; var propertyModelName = ModelNames.CreatePropertyModelName( context.ModelBindingContext.ModelName, propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName); var propertyModelBindingContext = ModelBindingContext.GetChildModelBindingContext( context.ModelBindingContext, propertyModelName, propertyMetadata); // If any property can return a true value. if (await CanBindValue(propertyModelBindingContext)) { return true; } } } if (!isAnyPropertyEnabledForValueProviderBasedBinding) { // Either there are no properties or all the properties are marked as // a non value provider based marker. // This would be the case when the model has all its properties annotated with // a IBinderMetadata. We want to be able to create such a model. return true; } return false; } private async Task CanBindValue(ModelBindingContext bindingContext) { var valueProvider = bindingContext.ValueProvider; var bindingSource = bindingContext.BindingSource; if (bindingSource != null && !bindingSource.IsGreedy) { var rootValueProvider = bindingContext.OperationBindingContext.ValueProvider as IBindingSourceValueProvider; if (rootValueProvider != null) { valueProvider = rootValueProvider.Filter(bindingSource); if (valueProvider == null) { // Unable to find a value provider for this binding source. Binding will fail. return false; } } } if (await valueProvider.ContainsPrefixAsync(bindingContext.ModelName)) { return true; } return false; } private static bool CanBindType(ModelMetadata modelMetadata) { // Simple types cannot use this binder if (!modelMetadata.IsComplexType) { return false; } if (modelMetadata.IsCollectionType) { return false; } return true; } internal static bool CanUpdatePropertyInternal(ModelMetadata propertyMetadata) { return !propertyMetadata.IsReadOnly || CanUpdateReadOnlyProperty(propertyMetadata.ModelType); } private static bool CanUpdateReadOnlyProperty(Type propertyType) { // Value types have copy-by-value semantics, which prevents us from updating // properties that are marked readonly. if (propertyType.GetTypeInfo().IsValueType) { return false; } // Arrays are strange beasts since their contents are mutable but their sizes aren't. // Therefore we shouldn't even try to update these. Further reading: // http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx if (propertyType.IsArray) { return false; } // Special-case known immutable reference types if (propertyType == typeof(string)) { return false; } return true; } // Returned dictionary contains entries corresponding to properties against which binding was attempted. If // binding failed, the entry's value will have IsModelSet == false. Binding is attempted for all elements of // propertyMetadatas. private async Task> BindPropertiesAsync( ModelBindingContext bindingContext, IEnumerable propertyMetadatas) { var results = new Dictionary(); foreach (var propertyMetadata in propertyMetadatas) { var propertyModelName = ModelNames.CreatePropertyModelName( bindingContext.ModelName, propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName); var childContext = ModelBindingContext.GetChildModelBindingContext( bindingContext, propertyModelName, propertyMetadata); // ModelBindingContext.Model property values may be non-null when invoked via TryUpdateModel(). Pass // complex (including collection) values down so that binding system does not unnecessarily recreate // instances or overwrite inner properties that are not bound. No need for this with simple values // because they will be overwritten if binding succeeds. Arrays are never reused because they cannot // be resized. // // ModelMetadata.PropertyGetter is not null safe; use it only if Model is non-null. if (bindingContext.Model != null && propertyMetadata.PropertyGetter != null && propertyMetadata.IsComplexType && !propertyMetadata.ModelType.IsArray) { childContext.Model = propertyMetadata.PropertyGetter(bindingContext.Model); } var result = await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childContext); if (result == null) { // Could not bind. Let ProcessResult() know explicitly. result = new ModelBindingResult(model: null, key: propertyModelName, isModelSet: false); } results[propertyMetadata] = result; } return results; } /// /// Creates suitable for given . /// /// The . /// An compatible with . protected virtual object CreateModel([NotNull] ModelBindingContext bindingContext) { // If the Activator throws an exception, we want to propagate it back up the call stack, since the // application developer should know that this was an invalid type to try to bind to. return Activator.CreateInstance(bindingContext.ModelType); } /// /// Get if that property is not null. Otherwise activate a /// new instance of . /// /// The . protected virtual object GetModel([NotNull] ModelBindingContext bindingContext) { if (bindingContext.Model != null) { return bindingContext.Model; } return CreateModel(bindingContext); } /// /// Gets the collection of for properties this binder should update. /// /// The . /// Collection of for properties this binder should update. protected virtual IEnumerable GetMetadataForProperties( [NotNull] ModelBindingContext bindingContext) { var validationInfo = GetPropertyValidationInfo(bindingContext); var newPropertyFilter = GetPropertyFilter(); return bindingContext.ModelMetadata.Properties .Where(propertyMetadata => newPropertyFilter(bindingContext, propertyMetadata.PropertyName) && (validationInfo.RequiredProperties.Contains(propertyMetadata.PropertyName) || !validationInfo.SkipProperties.Contains(propertyMetadata.PropertyName)) && CanUpdateProperty(propertyMetadata)); } private static Func GetPropertyFilter() { return (ModelBindingContext context, string propertyName) => { var modelMetadataPredicate = context.ModelMetadata.PropertyBindingPredicateProvider?.PropertyFilter; return context.PropertyFilter(context, propertyName) && (modelMetadataPredicate == null || modelMetadataPredicate(context, propertyName)); }; } internal static PropertyValidationInfo GetPropertyValidationInfo(ModelBindingContext bindingContext) { var validationInfo = new PropertyValidationInfo(); foreach (var propertyMetadata in bindingContext.ModelMetadata.Properties) { var propertyName = propertyMetadata.PropertyName; if (!propertyMetadata.IsBindingAllowed) { // Nothing to do here if binding is not allowed. validationInfo.SkipProperties.Add(propertyName); continue; } if (propertyMetadata.IsBindingRequired) { validationInfo.RequiredProperties.Add(propertyName); } } return validationInfo; } // Internal for testing. internal ModelValidationNode ProcessResults( ModelBindingContext bindingContext, IDictionary results, ModelValidationNode validationNode) { var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var modelExplorer = metadataProvider.GetModelExplorerForType(bindingContext.ModelType, bindingContext.Model); var validationInfo = GetPropertyValidationInfo(bindingContext); // Eliminate provided properties from RequiredProperties; leaving just *missing* required properties. var boundProperties = results.Where(p => p.Value.IsModelSet).Select(p => p.Key.PropertyName); validationInfo.RequiredProperties.ExceptWith(boundProperties); foreach (var missingRequiredProperty in validationInfo.RequiredProperties) { var propertyExplorer = modelExplorer.GetExplorerForProperty(missingRequiredProperty); var propertyName = propertyExplorer.Metadata.BinderModelName ?? missingRequiredProperty; var modelStateKey = ModelNames.CreatePropertyModelName(bindingContext.ModelName, propertyName); bindingContext.ModelState.TryAddModelError( modelStateKey, Resources.FormatModelBinding_MissingBindRequiredMember(propertyName)); } // For each property that BindPropertiesAsync() attempted to bind, call the setter, recording // exceptions as necessary. foreach (var entry in results) { var result = entry.Value; if (result != null) { var propertyMetadata = entry.Key; SetProperty(bindingContext, modelExplorer, propertyMetadata, result); var propertyValidationNode = result.ValidationNode; if (propertyValidationNode == null) { // Make sure that irrespective of whether the properties of the model were bound with a value, // create a validation node so that these get validated. propertyValidationNode = new ModelValidationNode(result.Key, entry.Key, result.Model); } validationNode.ChildNodes.Add(propertyValidationNode); } } return validationNode; } /// /// Updates a property in the current . /// /// The . /// /// The for the model containing property to set. /// /// The for the property to set. /// The for the property's new value. /// Should succeed in all cases that returns true. protected virtual void SetProperty( [NotNull] ModelBindingContext bindingContext, [NotNull] ModelExplorer modelExplorer, [NotNull] ModelMetadata propertyMetadata, [NotNull] ModelBindingResult result) { var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase; var property = bindingContext.ModelType.GetProperty(propertyMetadata.PropertyName, bindingFlags); if (property == null) { // Nothing to do if property does not exist. return; } if (!result.IsModelSet) { // If we don't have a value, don't set it on the model and trounce a pre-initialized value. return; } if (!property.CanWrite) { // Try to handle as a collection if property exists but is not settable. AddToProperty(bindingContext, modelExplorer, property, result); return; } var value = result.Model; try { propertyMetadata.PropertySetter(bindingContext.Model, value); } catch (Exception exception) { AddModelError(exception, bindingContext, result); } } private void AddToProperty( ModelBindingContext bindingContext, ModelExplorer modelExplorer, PropertyInfo property, ModelBindingResult result) { var propertyExplorer = modelExplorer.GetExplorerForProperty(property.Name); var target = propertyExplorer.Model; var source = result.Model; if (target == null || source == null) { // Cannot copy to or from a null collection. return; } if (target == source) { // Added to the target collection in BindPropertiesAsync(). return; } // Determine T if this is an ICollection property. No need for a T[] case because CanUpdateProperty() // ensures property is either settable or not an array. Underlying assumption is that CanUpdateProperty() // and SetProperty() are overridden together. var collectionTypeArguments = ClosedGenericMatcher.ExtractGenericInterface( propertyExplorer.ModelType, typeof(ICollection<>)) ?.GenericTypeArguments; if (collectionTypeArguments == null) { // Not a collection model. return; } var propertyAddRange = CallPropertyAddRangeOpenGenericMethod.MakeGenericMethod(collectionTypeArguments); try { propertyAddRange.Invoke(obj: null, parameters: new[] { target, source }); } catch (Exception exception) { AddModelError(exception, bindingContext, result); } } // Called via reflection. private static void CallPropertyAddRange(object target, object source) { var targetCollection = (ICollection)target; var sourceCollection = source as IEnumerable; if (sourceCollection != null && !targetCollection.IsReadOnly) { targetCollection.Clear(); foreach (var item in sourceCollection) { targetCollection.Add(item); } } } private static void AddModelError( Exception exception, ModelBindingContext bindingContext, ModelBindingResult result) { var targetInvocationException = exception as TargetInvocationException; if (targetInvocationException != null && targetInvocationException.InnerException != null) { exception = targetInvocationException.InnerException; } // Do not add an error message if a binding error has already occurred for this property. var modelState = bindingContext.ModelState; var modelStateKey = result.Key; var validationState = modelState.GetFieldValidationState(modelStateKey); if (validationState == ModelValidationState.Unvalidated) { modelState.AddModelError(modelStateKey, exception); } } // Returns true if validator execution adds a model error. private static bool RunValidator( IModelValidator validator, ModelBindingContext bindingContext, ModelExplorer propertyExplorer, string modelStateKey) { var validationContext = new ModelValidationContext(bindingContext, propertyExplorer); var addedError = false; foreach (var validationResult in validator.Validate(validationContext)) { bindingContext.ModelState.TryAddModelError(modelStateKey, validationResult.Message); addedError = true; } if (!addedError) { bindingContext.ModelState.MarkFieldValid(modelStateKey); } return addedError; } internal sealed class PropertyValidationInfo { public PropertyValidationInfo() { RequiredProperties = new HashSet(StringComparer.OrdinalIgnoreCase); SkipProperties = new HashSet(StringComparer.OrdinalIgnoreCase); } public HashSet RequiredProperties { get; private set; } public HashSet SkipProperties { get; private set; } } } }