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