// 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.ComponentModel; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.ModelBinding.Internal; using Microsoft.AspNet.Mvc.ModelBinding.Metadata; using Microsoft.AspNet.Mvc.ModelBinding.Validation; namespace Microsoft.AspNet.Mvc.ModelBinding { public class MutableObjectModelBinder : IModelBinder { public virtual async Task BindModelAsync(ModelBindingContext bindingContext) { ModelBindingHelper.ValidateBindingContext(bindingContext); if (!CanBindType(bindingContext.ModelMetadata)) { return null; } var mutableObjectBinderContext = new MutableObjectBinderContext() { ModelBindingContext = bindingContext, PropertyMetadata = GetMetadataForProperties(bindingContext), }; if (!(await CanCreateModel(mutableObjectBinderContext))) { return null; } EnsureModel(bindingContext); var result = await CreateAndPopulateDto(bindingContext, mutableObjectBinderContext.PropertyMetadata); // post-processing, e.g. property setters and hooking up validation ProcessDto(bindingContext, (ComplexModelDto)result.Model); return new ModelBindingResult( bindingContext.Model, bindingContext.ModelName, isModelSet: true); } protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata) { return CanUpdatePropertyInternal(propertyMetadata); } internal async Task CanCreateModel(MutableObjectBinderContext context) { var bindingContext = context.ModelBindingContext; var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null; var hasExplicitAlias = bindingContext.BinderModelName != null; // 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 with an explicit user supplied prefix. // In this case since it will never fallback to empty prefix, we need to create the model here. if (isTopLevelObject && hasExplicitAlias) { return true; } // 2. It is a top level object and there is no model name ( Fallback to empty prefix case ). // This is necessary as we do not want to depend on a value provider to contain an empty prefix. if (isTopLevelObject && bindingContext.ModelName == string.Empty) { 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) { // 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 = ModelBindingHelper.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 (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.ModelType == typeof(ComplexModelDto)) { // forbidden type - will cause a stack overflow if we try binding this type 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; } private async Task CreateAndPopulateDto( ModelBindingContext bindingContext, IEnumerable propertyMetadatas) { // create a DTO and call into the DTO binder var dto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas); var metadataProvider = bindingContext.OperationBindingContext.MetadataProvider; var dtoMetadata = metadataProvider.GetMetadataForType(typeof(ComplexModelDto)); var childContext = ModelBindingContext.GetChildModelBindingContext( bindingContext, bindingContext.ModelName, dtoMetadata); childContext.Model = dto; return await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(childContext); } protected virtual object CreateModel(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); } protected virtual void EnsureModel(ModelBindingContext bindingContext) { if (bindingContext.Model == null) { bindingContext.Model = CreateModel(bindingContext); } } protected virtual IEnumerable GetMetadataForProperties(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)); }; } private static bool TryGetPropertyDefaultValue(PropertyInfo propertyInfo, out object value) { var attribute = propertyInfo.GetCustomAttribute(); if (attribute == null) { value = null; return false; } else { value = attribute.Value; return true; } } internal static PropertyValidationInfo GetPropertyValidationInfo(ModelBindingContext bindingContext) { var validationInfo = new PropertyValidationInfo(); var modelTypeInfo = bindingContext.ModelType.GetTypeInfo(); var typeAttribute = modelTypeInfo.GetCustomAttribute(); var properties = bindingContext.ModelType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var property in properties) { var propertyName = property.Name; var propertyMetadata = bindingContext.ModelMetadata.Properties[propertyName]; if (propertyMetadata == null) { // Skip indexer properties and others ModelMetadata ignores. continue; } var validatorProviderContext = new ModelValidatorProviderContext(propertyMetadata); bindingContext.OperationBindingContext.ValidatorProvider.GetValidators(validatorProviderContext); var requiredValidator = validatorProviderContext.Validators .FirstOrDefault(v => v != null && v.IsRequired); if (requiredValidator != null) { validationInfo.RequiredValidators[propertyName] = requiredValidator; } var propertyAttribute = property.GetCustomAttribute(); var bindingBehaviorAttribute = propertyAttribute ?? typeAttribute; if (bindingBehaviorAttribute != null) { switch (bindingBehaviorAttribute.Behavior) { case BindingBehavior.Required: validationInfo.RequiredProperties.Add(propertyName); break; case BindingBehavior.Never: validationInfo.SkipProperties.Add(propertyName); break; } } else if (requiredValidator != null) { validationInfo.RequiredProperties.Add(propertyName); } else if (propertyMetadata.IsBindingRequired) { validationInfo.RequiredProperties.Add(propertyName); } } return validationInfo; } internal void ProcessDto(ModelBindingContext bindingContext, ComplexModelDto dto) { 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 = dto.Results.Where(p => p.Value.IsModelSet).Select(p => p.Key.PropertyName); validationInfo.RequiredProperties.ExceptWith(boundProperties); foreach (var missingRequiredProperty in validationInfo.RequiredProperties) { var addedError = false; // We want to provide the 'null' value, not the value of model.Property, // so avoiding modelExplorer.GetProperty here which would call the actual getter on the // model. This avoids issues with value types, or properties with pre-initialized values. var propertyExplorer = modelExplorer.GetExplorerForProperty(missingRequiredProperty, model: null); var propertyName = propertyExplorer.Metadata.BinderModelName ?? missingRequiredProperty; var modelStateKey = ModelBindingHelper.CreatePropertyModelName( bindingContext.ModelName, propertyName); // Execute validator (if any) to get custom error message. IModelValidator validator; if (validationInfo.RequiredValidators.TryGetValue(missingRequiredProperty, out validator)) { addedError = RunValidator(validator, bindingContext, propertyExplorer, modelStateKey); } // Fall back to default message if BindingBehaviorAttribute required this property or validator // (oddly) succeeded. if (!addedError) { bindingContext.ModelState.TryAddModelError( modelStateKey, Resources.FormatMissingRequiredMember(propertyName)); } } // For each property that ComplexModelDtoModelBinder attempted to bind, call the setter, recording // exceptions as necessary. foreach (var entry in dto.Results) { var dtoResult = entry.Value; if (dtoResult != null) { var propertyMetadata = entry.Key; IModelValidator requiredValidator; validationInfo.RequiredValidators.TryGetValue( propertyMetadata.PropertyName, out requiredValidator); SetProperty(bindingContext, modelExplorer, propertyMetadata, dtoResult, requiredValidator); } } } protected virtual void SetProperty( ModelBindingContext bindingContext, ModelExplorer modelExplorer, ModelMetadata propertyMetadata, ModelBindingResult dtoResult, IModelValidator requiredValidator) { var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase; var property = bindingContext.ModelType.GetProperty( propertyMetadata.PropertyName, bindingFlags); if (property == null || !property.CanWrite) { // nothing to do return; } object value; var hasDefaultValue = false; if (dtoResult.IsModelSet) { value = dtoResult.Model; } else { hasDefaultValue = TryGetPropertyDefaultValue(property, out value); } // 'Required' validators need to run first so that we can provide useful error messages if // the property setters throw, e.g. if we're setting entity keys to null. if (value == null) { var modelStateKey = dtoResult.Key; var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey); if (validationState == ModelValidationState.Unvalidated) { if (requiredValidator != null) { var propertyExplorer = modelExplorer.GetExplorerForExpression(propertyMetadata, model: null); var validationContext = new ModelValidationContext(bindingContext, propertyExplorer); foreach (var validationResult in requiredValidator.Validate(validationContext)) { bindingContext.ModelState.TryAddModelError(modelStateKey, validationResult.Message); } } } } if (!dtoResult.IsModelSet && !hasDefaultValue) { // If we don't have a value, don't set it on the model and trounce a pre-initialized // value. return; } if (value != null || property.PropertyType.AllowsNullValue()) { try { propertyMetadata.PropertySetter(bindingContext.Model, value); } catch (Exception ex) { // don't display a duplicate error message if a binding error has already occurred for this field var targetInvocationException = ex as TargetInvocationException; if (targetInvocationException != null && targetInvocationException.InnerException != null) { ex = targetInvocationException.InnerException; } var modelStateKey = dtoResult.Key; var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey); if (validationState == ModelValidationState.Unvalidated) { bindingContext.ModelState.AddModelError(modelStateKey, ex); } } } else { // trying to set a non-nullable value type to null, need to make sure there's a message var modelStateKey = dtoResult.Key; var validationState = bindingContext.ModelState.GetFieldValidationState(modelStateKey); if (validationState == ModelValidationState.Unvalidated) { var errorMessage = Resources.ModelBinderConfig_ValueRequired; bindingContext.ModelState.TryAddModelError(modelStateKey, errorMessage); } } } // 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); RequiredValidators = new Dictionary(StringComparer.OrdinalIgnoreCase); SkipProperties = new HashSet(StringComparer.OrdinalIgnoreCase); } public HashSet RequiredProperties { get; private set; } public Dictionary RequiredValidators { get; private set; } public HashSet SkipProperties { get; private set; } } } }