1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. 2 3 using System; 4 using System.Collections.Generic; 5 using System.ComponentModel; 6 using System.Diagnostics.CodeAnalysis; 7 using System.Linq; 8 using System.Web.Mvc; 9 10 namespace Microsoft.Web.Mvc.ModelBinding 11 { 12 public class MutableObjectModelBinder : IExtensibleModelBinder 13 { 14 private ModelMetadataProvider _metadataProvider; 15 16 internal ModelMetadataProvider MetadataProvider 17 { 18 get 19 { 20 if (_metadataProvider == null) 21 { 22 _metadataProvider = ModelMetadataProviders.Current; 23 } 24 return _metadataProvider; 25 } 26 set { _metadataProvider = value; } 27 } 28 BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)29 public virtual bool BindModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext) 30 { 31 ModelBinderUtil.ValidateBindingContext(bindingContext); 32 33 EnsureModel(controllerContext, bindingContext); 34 IEnumerable<ModelMetadata> propertyMetadatas = GetMetadataForProperties(controllerContext, bindingContext); 35 ComplexModelDto dto = CreateAndPopulateDto(controllerContext, bindingContext, propertyMetadatas); 36 37 // post-processing, e.g. property setters and hooking up validation 38 ProcessDto(controllerContext, bindingContext, dto); 39 bindingContext.ValidationNode.ValidateAllProperties = true; // complex models require full validation 40 return true; 41 } 42 CanUpdateProperty(ModelMetadata propertyMetadata)43 protected virtual bool CanUpdateProperty(ModelMetadata propertyMetadata) 44 { 45 return CanUpdatePropertyInternal(propertyMetadata); 46 } 47 CanUpdatePropertyInternal(ModelMetadata propertyMetadata)48 internal static bool CanUpdatePropertyInternal(ModelMetadata propertyMetadata) 49 { 50 return (!propertyMetadata.IsReadOnly || CanUpdateReadOnlyProperty(propertyMetadata.ModelType)); 51 } 52 CanUpdateReadOnlyProperty(Type propertyType)53 private static bool CanUpdateReadOnlyProperty(Type propertyType) 54 { 55 // Value types have copy-by-value semantics, which prevents us from updating 56 // properties that are marked readonly. 57 if (propertyType.IsValueType) 58 { 59 return false; 60 } 61 62 // Arrays are strange beasts since their contents are mutable but their sizes aren't. 63 // Therefore we shouldn't even try to update these. Further reading: 64 // http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx 65 if (propertyType.IsArray) 66 { 67 return false; 68 } 69 70 // Special-case known immutable reference types 71 if (propertyType == typeof(string)) 72 { 73 return false; 74 } 75 76 return true; 77 } 78 CreateAndPopulateDto(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, IEnumerable<ModelMetadata> propertyMetadatas)79 private ComplexModelDto CreateAndPopulateDto(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, IEnumerable<ModelMetadata> propertyMetadatas) 80 { 81 // create a DTO and call into the DTO binder 82 ComplexModelDto originalDto = new ComplexModelDto(bindingContext.ModelMetadata, propertyMetadatas); 83 ExtensibleModelBindingContext dtoBindingContext = new ExtensibleModelBindingContext(bindingContext) 84 { 85 ModelMetadata = MetadataProvider.GetMetadataForType(() => originalDto, typeof(ComplexModelDto)), 86 ModelName = bindingContext.ModelName 87 }; 88 89 IExtensibleModelBinder dtoBinder = bindingContext.ModelBinderProviders.GetRequiredBinder(controllerContext, dtoBindingContext); 90 dtoBinder.BindModel(controllerContext, dtoBindingContext); 91 return (ComplexModelDto)dtoBindingContext.Model; 92 } 93 CreateModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)94 protected virtual object CreateModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext) 95 { 96 // If the Activator throws an exception, we want to propagate it back up the call stack, since the application 97 // developer should know that this was an invalid type to try to bind to. 98 return Activator.CreateInstance(bindingContext.ModelType); 99 } 100 101 // Called when the property setter null check failed, allows us to add our own error message to ModelState. CreateNullCheckFailedHandler(ControllerContext controllerContext, ModelMetadata modelMetadata, object incomingValue)102 internal static EventHandler<ModelValidatedEventArgs> CreateNullCheckFailedHandler(ControllerContext controllerContext, ModelMetadata modelMetadata, object incomingValue) 103 { 104 return (sender, e) => 105 { 106 ModelValidationNode validationNode = (ModelValidationNode)sender; 107 ModelStateDictionary modelState = e.ControllerContext.Controller.ViewData.ModelState; 108 109 if (modelState.IsValidField(validationNode.ModelStateKey)) 110 { 111 string errorMessage = ModelBinderConfig.ValueRequiredErrorMessageProvider(controllerContext, modelMetadata, incomingValue); 112 if (errorMessage != null) 113 { 114 modelState.AddModelError(validationNode.ModelStateKey, errorMessage); 115 } 116 } 117 }; 118 } 119 EnsureModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)120 protected virtual void EnsureModel(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext) 121 { 122 if (bindingContext.Model == null) 123 { 124 bindingContext.ModelMetadata.Model = CreateModel(controllerContext, bindingContext); 125 } 126 } 127 GetMetadataForProperties(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext)128 protected virtual IEnumerable<ModelMetadata> GetMetadataForProperties(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext) 129 { 130 // keep a set of the required properties so that we can cross-reference bound properties later 131 HashSet<string> requiredProperties; 132 HashSet<string> skipProperties; 133 GetRequiredPropertiesCollection(bindingContext.ModelType, out requiredProperties, out skipProperties); 134 135 return from propertyMetadata in bindingContext.ModelMetadata.Properties 136 let propertyName = propertyMetadata.PropertyName 137 let shouldUpdateProperty = requiredProperties.Contains(propertyName) || !skipProperties.Contains(propertyName) 138 where shouldUpdateProperty && CanUpdateProperty(propertyMetadata) 139 select propertyMetadata; 140 } 141 GetPropertyDefaultValue(PropertyDescriptor propertyDescriptor)142 private static object GetPropertyDefaultValue(PropertyDescriptor propertyDescriptor) 143 { 144 DefaultValueAttribute attr = propertyDescriptor.Attributes.OfType<DefaultValueAttribute>().FirstOrDefault(); 145 return (attr != null) ? attr.Value : null; 146 } 147 GetRequiredPropertiesCollection(Type modelType, out HashSet<string> requiredProperties, out HashSet<string> skipProperties)148 internal static void GetRequiredPropertiesCollection(Type modelType, out HashSet<string> requiredProperties, out HashSet<string> skipProperties) 149 { 150 requiredProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase); 151 skipProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase); 152 153 // Use attributes on the property before attributes on the type. 154 ICustomTypeDescriptor modelDescriptor = TypeDescriptorHelper.Get(modelType); 155 PropertyDescriptorCollection propertyDescriptors = modelDescriptor.GetProperties(); 156 BindingBehaviorAttribute typeAttr = modelDescriptor.GetAttributes().OfType<BindingBehaviorAttribute>().SingleOrDefault(); 157 158 foreach (PropertyDescriptor propertyDescriptor in propertyDescriptors) 159 { 160 BindingBehaviorAttribute propAttr = propertyDescriptor.Attributes.OfType<BindingBehaviorAttribute>().SingleOrDefault(); 161 BindingBehaviorAttribute workingAttr = propAttr ?? typeAttr; 162 if (workingAttr != null) 163 { 164 switch (workingAttr.Behavior) 165 { 166 case BindingBehavior.Required: 167 requiredProperties.Add(propertyDescriptor.Name); 168 break; 169 170 case BindingBehavior.Never: 171 skipProperties.Add(propertyDescriptor.Name); 172 break; 173 } 174 } 175 } 176 } 177 ProcessDto(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, ComplexModelDto dto)178 internal void ProcessDto(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, ComplexModelDto dto) 179 { 180 HashSet<string> requiredProperties; 181 HashSet<string> skipProperties; 182 GetRequiredPropertiesCollection(bindingContext.ModelType, out requiredProperties, out skipProperties); 183 184 // Are all of the required fields accounted for? 185 HashSet<string> missingRequiredProperties = new HashSet<string>(requiredProperties); 186 missingRequiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName)); 187 string missingPropertyName = missingRequiredProperties.FirstOrDefault(); 188 if (missingPropertyName != null) 189 { 190 string fullPropertyKey = ModelBinderUtil.CreatePropertyModelName(bindingContext.ModelName, missingPropertyName); 191 throw Error.BindingBehavior_ValueNotFound(fullPropertyKey); 192 } 193 194 // for each property that was bound, call the setter, recording exceptions as necessary 195 foreach (var entry in dto.Results) 196 { 197 ModelMetadata propertyMetadata = entry.Key; 198 199 ComplexModelDtoResult dtoResult = entry.Value; 200 if (dtoResult != null) 201 { 202 SetProperty(controllerContext, bindingContext, propertyMetadata, dtoResult); 203 bindingContext.ValidationNode.ChildNodes.Add(dtoResult.ValidationNode); 204 } 205 } 206 } 207 208 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")] SetProperty(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult)209 protected virtual void SetProperty(ControllerContext controllerContext, ExtensibleModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult) 210 { 211 PropertyDescriptor propertyDescriptor = TypeDescriptorHelper.Get(bindingContext.ModelType).GetProperties().Find(propertyMetadata.PropertyName, true /* ignoreCase */); 212 if (propertyDescriptor == null || propertyDescriptor.IsReadOnly) 213 { 214 return; // nothing to do 215 } 216 217 object value = dtoResult.Model ?? GetPropertyDefaultValue(propertyDescriptor); 218 propertyMetadata.Model = value; 219 220 // 'Required' validators need to run first so that we can provide useful error messages if 221 // the property setters throw, e.g. if we're setting entity keys to null. See comments in 222 // DefaultModelBinder.SetProperty() for more information. 223 if (value == null) 224 { 225 string modelStateKey = dtoResult.ValidationNode.ModelStateKey; 226 if (bindingContext.ModelState.IsValidField(modelStateKey)) 227 { 228 ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, controllerContext).Where(v => v.IsRequired).FirstOrDefault(); 229 if (requiredValidator != null) 230 { 231 foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model)) 232 { 233 bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message); 234 } 235 } 236 } 237 } 238 239 if (value != null || TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType)) 240 { 241 try 242 { 243 propertyDescriptor.SetValue(bindingContext.Model, value); 244 } 245 catch (Exception ex) 246 { 247 // don't display a duplicate error message if a binding error has already occurred for this field 248 string modelStateKey = dtoResult.ValidationNode.ModelStateKey; 249 if (bindingContext.ModelState.IsValidField(modelStateKey)) 250 { 251 bindingContext.ModelState.AddModelError(modelStateKey, ex); 252 } 253 } 254 } 255 else 256 { 257 // trying to set a non-nullable value type to null, need to make sure there's a message 258 string modelStateKey = dtoResult.ValidationNode.ModelStateKey; 259 if (bindingContext.ModelState.IsValidField(modelStateKey)) 260 { 261 dtoResult.ValidationNode.Validated += CreateNullCheckFailedHandler(controllerContext, propertyMetadata, value); 262 } 263 } 264 } 265 } 266 } 267