File: ModelBinding\MutableObjectModelBinder.cs
Project: ndp\fx\src\xsp\system\Web\System.Web.csproj (System.Web)
namespace System.Web.ModelBinding {
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Runtime.CompilerServices;
 
    public class MutableObjectModelBinder : IModelBinder {
 
        private ModelMetadataProvider _metadataProvider;
 
        internal ModelMetadataProvider MetadataProvider {
            get {
                if (_metadataProvider == null) {
                    _metadataProvider = ModelMetadataProviders.Current;
                }
                return _metadataProvider;
            }
            set {
                _metadataProvider = value;
            }
        }
 
        public virtual bool BindModel(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext) {
            // Recursive method - prevent stack overflow.
            RuntimeHelpers.EnsureSufficientExecutionStack();
 
            ModelBinderUtil.ValidateBindingContext(bindingContext);
 
            EnsureModel(modelBindingExecutionContext, bindingContext);
            IEnumerable<ModelMetadata> propertyMetadatas = GetMetadataForProperties(modelBindingExecutionContext, bindingContext);
            ComplexModel complexModel = CreateAndPopulateComplexModel(modelBindingExecutionContext, bindingContext, propertyMetadatas);
 
            // post-processing, e.g. property setters and hooking up validation
            ProcessComplexModel(modelBindingExecutionContext, bindingContext, complexModel);
            bindingContext.ValidationNode.ValidateAllProperties = true; // complex models require full validation
            return true;
        }
 
        protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata) {
            return CanUpdatePropertyInternal(propertyMetadata);
        }
 
        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.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 ComplexModel CreateAndPopulateComplexModel(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext, IEnumerable<ModelMetadata> propertyMetadatas) {
            // create a Complex Model and call into the Complex Model binder
            ComplexModel originalComplexModel = new ComplexModel(bindingContext.ModelMetadata, propertyMetadatas);
            ModelBindingContext complexModelBindingContext = new ModelBindingContext(bindingContext) {
                ModelMetadata = MetadataProvider.GetMetadataForType(() => originalComplexModel, typeof(ComplexModel)),
                ModelName = bindingContext.ModelName
            };
 
            IModelBinder complexModelBinder = bindingContext.ModelBinderProviders.GetRequiredBinder(modelBindingExecutionContext, complexModelBindingContext);
            complexModelBinder.BindModel(modelBindingExecutionContext, complexModelBindingContext);
            return (ComplexModel)complexModelBindingContext.Model;
        }
 
        protected virtual object CreateModel(ModelBindingExecutionContext modelBindingExecutionContext, 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 SecurityUtils.SecureCreateInstance(bindingContext.ModelType);
        }
 
        // Called when the property setter null check failed, allows us to add our own error message to ModelState.
        internal static EventHandler<ModelValidatedEventArgs> CreateNullCheckFailedHandler(ModelBindingExecutionContext modelBindingExecutionContext, ModelMetadata modelMetadata, object incomingValue) {
            return (sender, e) =>
            {
                ModelValidationNode validationNode = (ModelValidationNode)sender;
                ModelStateDictionary modelState = e.ModelBindingExecutionContext.ModelState;
 
                if (modelState.IsValidField(validationNode.ModelStateKey)) {
                    string errorMessage = ModelBinderErrorMessageProviders.ValueRequiredErrorMessageProvider(modelBindingExecutionContext, modelMetadata, incomingValue);
                    if (errorMessage != null) {
                        modelState.AddModelError(validationNode.ModelStateKey, errorMessage);
                    }
                }
            };
        }
 
        protected virtual void EnsureModel(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext) {
            if (bindingContext.Model == null) {
                bindingContext.ModelMetadata.Model = CreateModel(modelBindingExecutionContext, bindingContext);
            }
        }
 
        protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext) {
            // keep a set of the required properties so that we can cross-reference bound properties later
            HashSet<string> requiredProperties;
            HashSet<string> skipProperties;
            GetRequiredPropertiesCollection(bindingContext.ModelType, out requiredProperties, out skipProperties);
 
            return from propertyMetadata in bindingContext.ModelMetadata.Properties
                   let propertyName = propertyMetadata.PropertyName
                   let shouldUpdateProperty = requiredProperties.Contains(propertyName) || !skipProperties.Contains(propertyName)
                   where shouldUpdateProperty && CanUpdateProperty(propertyMetadata)
                   select propertyMetadata;
        }
 
        private static object GetPropertyDefaultValue(PropertyDescriptor propertyDescriptor) {
            DefaultValueAttribute attr = propertyDescriptor.Attributes.OfType<DefaultValueAttribute>().FirstOrDefault();
            return (attr != null) ? attr.Value : null;
        }
 
        internal static void GetRequiredPropertiesCollection(Type modelType, out HashSet<string> requiredProperties, out HashSet<string> skipProperties) {
            requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            skipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
            // Use attributes on the property before attributes on the type.
            ICustomTypeDescriptor modelDescriptor = TypeDescriptorHelper.Get(modelType);
            PropertyDescriptorCollection propertyDescriptors = modelDescriptor.GetProperties();
            BindingBehaviorAttribute typeAttr = modelDescriptor.GetAttributes().OfType<BindingBehaviorAttribute>().SingleOrDefault();
 
            foreach (PropertyDescriptor propertyDescriptor in propertyDescriptors) {
                BindingBehaviorAttribute propAttr = propertyDescriptor.Attributes.OfType<BindingBehaviorAttribute>().SingleOrDefault();
                BindingBehaviorAttribute workingAttr = propAttr ?? typeAttr;
                if (workingAttr != null) {
                    switch (workingAttr.Behavior) {
                        case BindingBehavior.Required:
                            requiredProperties.Add(propertyDescriptor.Name);
                            break;
 
                        case BindingBehavior.Never:
                            skipProperties.Add(propertyDescriptor.Name);
                            break;
                    }
                }
            }
        }
 
        internal void ProcessComplexModel(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext, ComplexModel complexModel) {
            HashSet<string> requiredProperties;
            HashSet<string> skipProperties;
            GetRequiredPropertiesCollection(bindingContext.ModelType, out requiredProperties, out skipProperties);
 
            // Are all of the required fields accounted for?
            HashSet<string> missingRequiredProperties = new HashSet<string>(requiredProperties);
            missingRequiredProperties.ExceptWith(complexModel.Results.Select(r => r.Key.PropertyName));
            string missingPropertyName = missingRequiredProperties.FirstOrDefault();
            if (missingPropertyName != null) {
                string fullPropertyKey = ModelBinderUtil.CreatePropertyModelName(bindingContext.ModelName, missingPropertyName);
                throw Error.BindingBehavior_ValueNotFound(fullPropertyKey);
            }
 
            // for each property that was bound, call the setter, recording exceptions as necessary
            foreach (var entry in complexModel.Results) {
                ModelMetadata propertyMetadata = entry.Key;
 
                ComplexModelResult complexModelResult = entry.Value;
                if (complexModelResult != null) {
                    SetProperty(modelBindingExecutionContext, bindingContext, propertyMetadata, complexModelResult);
                    bindingContext.ValidationNode.ChildNodes.Add(complexModelResult.ValidationNode);
                }
            }
        }
 
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
        protected virtual void SetProperty(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelResult complexModelResult) {
            PropertyDescriptor propertyDescriptor = TypeDescriptorHelper.Get(bindingContext.ModelType).GetProperties().Find(propertyMetadata.PropertyName, true /* ignoreCase */);
            if (propertyDescriptor == null || propertyDescriptor.IsReadOnly) {
                return; // nothing to do
            }
 
            object value = complexModelResult.Model ?? GetPropertyDefaultValue(propertyDescriptor);
            propertyMetadata.Model = 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. See comments in
            // DefaultModelBinder.SetProperty() for more information.
            if (value == null) {
                string modelStateKey = complexModelResult.ValidationNode.ModelStateKey;
                if (bindingContext.ModelState.IsValidField(modelStateKey)) {
                    ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, modelBindingExecutionContext).Where(v => v.IsRequired).FirstOrDefault();
                    if (requiredValidator != null) {
                        foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model)) {
                            bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
                        }
                    }
                }
            }
 
            if (value != null || TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType)) {
                try {
                    propertyDescriptor.SetValue(bindingContext.Model, value);
                }
                catch (Exception ex) {
                    // don't display a duplicate error message if a binding error has already occurred for this field
                    string modelStateKey = complexModelResult.ValidationNode.ModelStateKey;
                    if (bindingContext.ModelState.IsValidField(modelStateKey)) {
                        bindingContext.ModelState.AddModelError(modelStateKey, ex);
                    }
                }
            }
            else {
                // trying to set a non-nullable value type to null, need to make sure there's a message
                string modelStateKey = complexModelResult.ValidationNode.ModelStateKey;
                if (bindingContext.ModelState.IsValidField(modelStateKey)) {
                    complexModelResult.ValidationNode.Validated += CreateNullCheckFailedHandler(modelBindingExecutionContext, propertyMetadata, value);
                }
            }
        }
 
    }
}