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 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 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 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 GetMetadataForProperties(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext) { // keep a set of the required properties so that we can cross-reference bound properties later HashSet requiredProperties; HashSet 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().FirstOrDefault(); return (attr != null) ? attr.Value : null; } internal static void GetRequiredPropertiesCollection(Type modelType, out HashSet requiredProperties, out HashSet skipProperties) { requiredProperties = new HashSet(StringComparer.OrdinalIgnoreCase); skipProperties = new HashSet(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().SingleOrDefault(); foreach (PropertyDescriptor propertyDescriptor in propertyDescriptors) { BindingBehaviorAttribute propAttr = propertyDescriptor.Attributes.OfType().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 requiredProperties; HashSet skipProperties; GetRequiredPropertiesCollection(bindingContext.ModelType, out requiredProperties, out skipProperties); // Are all of the required fields accounted for? HashSet missingRequiredProperties = new HashSet(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); } } } } }