1 namespace System.Web.Mvc {
2     using System;
3     using System.Collections;
4     using System.Collections.Generic;
5     using System.ComponentModel;
6     using System.Diagnostics.CodeAnalysis;
7     using System.Globalization;
8     using System.Linq;
9     using System.Reflection;
10     using System.Runtime.CompilerServices;
11     using System.Web.Mvc.Resources;
12 
13     public class DefaultModelBinder : IModelBinder {
14 
15         private ModelBinderDictionary _binders;
16         private static string _resourceClassKey;
17 
18         [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "Property is settable so that the dictionary can be provided for unit testing purposes.")]
19         protected internal ModelBinderDictionary Binders {
20             get {
21                 if (_binders == null) {
22                     _binders = ModelBinders.Binders;
23                 }
24                 return _binders;
25             }
26             set {
27                 _binders = value;
28             }
29         }
30 
31         public static string ResourceClassKey {
32             get {
33                 return _resourceClassKey ?? String.Empty;
34             }
35             set {
36                 _resourceClassKey = value;
37             }
38         }
39 
AddValueRequiredMessageToModelState(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value)40         private static void AddValueRequiredMessageToModelState(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value) {
41             if (value == null && !TypeHelpers.TypeAllowsNullValue(elementType) && modelState.IsValidField(modelStateKey)) {
42                 modelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
43             }
44         }
45 
BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, object model)46         internal void BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) {
47             // need to replace the property filter + model object and create an inner binding context
48             ModelBindingContext newBindingContext = CreateComplexElementalModelBindingContext(controllerContext, bindingContext, model);
49 
50             // validation
51             if (OnModelUpdating(controllerContext, newBindingContext)) {
52                 BindProperties(controllerContext, newBindingContext);
53                 OnModelUpdated(controllerContext, newBindingContext);
54             }
55         }
56 
BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)57         internal object BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
58             object model = bindingContext.Model;
59             Type modelType = bindingContext.ModelType;
60 
61             // if we're being asked to create an array, create a list instead, then coerce to an array after the list is created
62             if (model == null && modelType.IsArray) {
63                 Type elementType = modelType.GetElementType();
64                 Type listType = typeof(List<>).MakeGenericType(elementType);
65                 object collection = CreateModel(controllerContext, bindingContext, listType);
66 
67                 ModelBindingContext arrayBindingContext = new ModelBindingContext() {
68                     ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => collection, listType),
69                     ModelName = bindingContext.ModelName,
70                     ModelState = bindingContext.ModelState,
71                     PropertyFilter = bindingContext.PropertyFilter,
72                     ValueProvider = bindingContext.ValueProvider
73                 };
74                 IList list = (IList)UpdateCollection(controllerContext, arrayBindingContext, elementType);
75 
76                 if (list == null) {
77                     return null;
78                 }
79 
80                 Array array = Array.CreateInstance(elementType, list.Count);
81                 list.CopyTo(array, 0);
82                 return array;
83             }
84 
85             if (model == null) {
86                 model = CreateModel(controllerContext, bindingContext, modelType);
87             }
88 
89             // special-case IDictionary<,> and ICollection<>
90             Type dictionaryType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IDictionary<,>));
91             if (dictionaryType != null) {
92                 Type[] genericArguments = dictionaryType.GetGenericArguments();
93                 Type keyType = genericArguments[0];
94                 Type valueType = genericArguments[1];
95 
96                 ModelBindingContext dictionaryBindingContext = new ModelBindingContext() {
97                     ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
98                     ModelName = bindingContext.ModelName,
99                     ModelState = bindingContext.ModelState,
100                     PropertyFilter = bindingContext.PropertyFilter,
101                     ValueProvider = bindingContext.ValueProvider
102                 };
103                 object dictionary = UpdateDictionary(controllerContext, dictionaryBindingContext, keyType, valueType);
104                 return dictionary;
105             }
106 
107             Type enumerableType = TypeHelpers.ExtractGenericInterface(modelType, typeof(IEnumerable<>));
108             if (enumerableType != null) {
109                 Type elementType = enumerableType.GetGenericArguments()[0];
110 
111                 Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
112                 if (collectionType.IsInstanceOfType(model)) {
113                     ModelBindingContext collectionBindingContext = new ModelBindingContext() {
114                         ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
115                         ModelName = bindingContext.ModelName,
116                         ModelState = bindingContext.ModelState,
117                         PropertyFilter = bindingContext.PropertyFilter,
118                         ValueProvider = bindingContext.ValueProvider
119                     };
120                     object collection = UpdateCollection(controllerContext, collectionBindingContext, elementType);
121                     return collection;
122                 }
123             }
124 
125             // otherwise, just update the properties on the complex type
126             BindComplexElementalModel(controllerContext, bindingContext, model);
127             return model;
128         }
129 
BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)130         public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
131             if (bindingContext == null) {
132                 throw new ArgumentNullException("bindingContext");
133             }
134 
135             bool performedFallback = false;
136 
137             if (!String.IsNullOrEmpty(bindingContext.ModelName) && !bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) {
138                 // We couldn't find any entry that began with the prefix. If this is the top-level element, fall back
139                 // to the empty prefix.
140                 if (bindingContext.FallbackToEmptyPrefix) {
141                     bindingContext = new ModelBindingContext() {
142                         ModelMetadata = bindingContext.ModelMetadata,
143                         ModelState = bindingContext.ModelState,
144                         PropertyFilter = bindingContext.PropertyFilter,
145                         ValueProvider = bindingContext.ValueProvider
146                     };
147                     performedFallback = true;
148                 }
149                 else {
150                     return null;
151                 }
152             }
153 
154             // Simple model = int, string, etc.; determined by calling TypeConverter.CanConvertFrom(typeof(string))
155             // or by seeing if a value in the request exactly matches the name of the model we're binding.
156             // Complex type = everything else.
157             if (!performedFallback) {
158                 bool performRequestValidation = ShouldPerformRequestValidation(controllerContext, bindingContext);
159                 ValueProviderResult vpResult = bindingContext.UnvalidatedValueProvider.GetValue(bindingContext.ModelName, skipValidation: !performRequestValidation);
160                 if (vpResult != null) {
161                     return BindSimpleModel(controllerContext, bindingContext, vpResult);
162                 }
163             }
164             if (!bindingContext.ModelMetadata.IsComplexType) {
165                 return null;
166             }
167 
168             return BindComplexModel(controllerContext, bindingContext);
169         }
170 
BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)171         private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
172             IEnumerable<PropertyDescriptor> properties = GetFilteredModelProperties(controllerContext, bindingContext);
173             foreach (PropertyDescriptor property in properties) {
174                 BindProperty(controllerContext, bindingContext, property);
175             }
176         }
177 
BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)178         protected virtual void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
179             // need to skip properties that aren't part of the request, else we might hit a StackOverflowException
180             string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
181             if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey)) {
182                 return;
183             }
184 
185             // call into the property's model binder
186             IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
187             object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
188             ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
189             propertyMetadata.Model = originalPropertyValue;
190             ModelBindingContext innerBindingContext = new ModelBindingContext() {
191                 ModelMetadata = propertyMetadata,
192                 ModelName = fullPropertyKey,
193                 ModelState = bindingContext.ModelState,
194                 ValueProvider = bindingContext.ValueProvider
195             };
196             object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
197             propertyMetadata.Model = newPropertyValue;
198 
199             // validation
200             ModelState modelState = bindingContext.ModelState[fullPropertyKey];
201             if (modelState == null || modelState.Errors.Count == 0) {
202                 if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) {
203                     SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
204                     OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
205                 }
206             }
207             else {
208                 SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
209 
210                 // Convert FormatExceptions (type conversion failures) into InvalidValue messages
211                 foreach (ModelError error in modelState.Errors.Where(err => String.IsNullOrEmpty(err.ErrorMessage) && err.Exception != null).ToList()) {
212                     for (Exception exception = error.Exception; exception != null; exception = exception.InnerException) {
213                         if (exception is FormatException) {
214                             string displayName = propertyMetadata.GetDisplayName();
215                             string errorMessageTemplate = GetValueInvalidResource(controllerContext);
216                             string errorMessage = String.Format(CultureInfo.CurrentCulture, errorMessageTemplate, modelState.Value.AttemptedValue, displayName);
217                             modelState.Errors.Remove(error);
218                             modelState.Errors.Add(errorMessage);
219                             break;
220                         }
221                     }
222                 }
223             }
224         }
225 
BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult)226         internal object BindSimpleModel(ControllerContext controllerContext, ModelBindingContext bindingContext, ValueProviderResult valueProviderResult) {
227             bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
228 
229             // if the value provider returns an instance of the requested data type, we can just short-circuit
230             // the evaluation and return that instance
231             if (bindingContext.ModelType.IsInstanceOfType(valueProviderResult.RawValue)) {
232                 return valueProviderResult.RawValue;
233             }
234 
235             // since a string is an IEnumerable<char>, we want it to skip the two checks immediately following
236             if (bindingContext.ModelType != typeof(string)) {
237 
238                 // conversion results in 3 cases, as below
239                 if (bindingContext.ModelType.IsArray) {
240                     // case 1: user asked for an array
241                     // ValueProviderResult.ConvertTo() understands array types, so pass in the array type directly
242                     object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
243                     return modelArray;
244                 }
245 
246                 Type enumerableType = TypeHelpers.ExtractGenericInterface(bindingContext.ModelType, typeof(IEnumerable<>));
247                 if (enumerableType != null) {
248                     // case 2: user asked for a collection rather than an array
249                     // need to call ConvertTo() on the array type, then copy the array to the collection
250                     object modelCollection = CreateModel(controllerContext, bindingContext, bindingContext.ModelType);
251                     Type elementType = enumerableType.GetGenericArguments()[0];
252                     Type arrayType = elementType.MakeArrayType();
253                     object modelArray = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, arrayType);
254 
255                     Type collectionType = typeof(ICollection<>).MakeGenericType(elementType);
256                     if (collectionType.IsInstanceOfType(modelCollection)) {
257                         CollectionHelpers.ReplaceCollection(elementType, modelCollection, modelArray);
258                     }
259                     return modelCollection;
260                 }
261             }
262 
263             // case 3: user asked for an individual element
264             object model = ConvertProviderResult(bindingContext.ModelState, bindingContext.ModelName, valueProviderResult, bindingContext.ModelType);
265             return model;
266         }
267 
CanUpdateReadonlyTypedReference(Type type)268         private static bool CanUpdateReadonlyTypedReference(Type type) {
269             // value types aren't strictly immutable, but because they have copy-by-value semantics
270             // we can't update a value type that is marked readonly
271             if (type.IsValueType) {
272                 return false;
273             }
274 
275             // arrays are mutable, but because we can't change their length we shouldn't try
276             // to update an array that is referenced readonly
277             if (type.IsArray) {
278                 return false;
279             }
280 
281             // special-case known common immutable types
282             if (type == typeof(string)) {
283                 return false;
284             }
285 
286             return true;
287         }
288 
289         [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "The target object should make the correct culture determination, not this method.")]
290         [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType)291         private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType) {
292             try {
293                 object convertedValue = valueProviderResult.ConvertTo(destinationType);
294                 return convertedValue;
295             }
296             catch (Exception ex) {
297                 modelState.AddModelError(modelStateKey, ex);
298                 return null;
299             }
300         }
301 
CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model)302         internal ModelBindingContext CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) {
303             BindAttribute bindAttr = (BindAttribute)GetTypeDescriptor(controllerContext, bindingContext).GetAttributes()[typeof(BindAttribute)];
304             Predicate<string> newPropertyFilter = (bindAttr != null)
305                 ? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName)
306                 : bindingContext.PropertyFilter;
307 
308             ModelBindingContext newBindingContext = new ModelBindingContext() {
309                 ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType),
310                 ModelName = bindingContext.ModelName,
311                 ModelState = bindingContext.ModelState,
312                 PropertyFilter = newPropertyFilter,
313                 ValueProvider = bindingContext.ValueProvider
314             };
315 
316             return newBindingContext;
317         }
318 
CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)319         protected virtual object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) {
320             Type typeToCreate = modelType;
321 
322             // we can understand some collection interfaces, e.g. IList<>, IDictionary<,>
323             if (modelType.IsGenericType) {
324                 Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
325                 if (genericTypeDefinition == typeof(IDictionary<,>)) {
326                     typeToCreate = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
327                 }
328                 else if (genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IList<>)) {
329                     typeToCreate = typeof(List<>).MakeGenericType(modelType.GetGenericArguments());
330                 }
331             }
332 
333             // fallback to the type's default constructor
334             return Activator.CreateInstance(typeToCreate);
335         }
336 
CreateSubIndexName(string prefix, int index)337         protected static string CreateSubIndexName(string prefix, int index) {
338             return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);
339         }
340 
CreateSubIndexName(string prefix, string index)341         protected static string CreateSubIndexName(string prefix, string index) {
342             return String.Format(CultureInfo.InvariantCulture, "{0}[{1}]", prefix, index);
343         }
344 
CreateSubPropertyName(string prefix, string propertyName)345         protected internal static string CreateSubPropertyName(string prefix, string propertyName) {
346             if (String.IsNullOrEmpty(prefix)) {
347                 return propertyName;
348             }
349             else if (String.IsNullOrEmpty(propertyName)) {
350                 return prefix;
351             }
352             else {
353                 return prefix + "." + propertyName;
354             }
355         }
356 
GetFilteredModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)357         protected IEnumerable<PropertyDescriptor> GetFilteredModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
358             PropertyDescriptorCollection properties = GetModelProperties(controllerContext, bindingContext);
359             Predicate<string> propertyFilter = bindingContext.PropertyFilter;
360 
361             return from PropertyDescriptor property in properties
362                    where ShouldUpdateProperty(property, propertyFilter)
363                    select property;
364         }
365 
366         [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "ValueProviderResult already handles culture conversion appropriately.")]
GetIndexes(ModelBindingContext bindingContext, out bool stopOnIndexNotFound, out IEnumerable<string> indexes)367         private static void GetIndexes(ModelBindingContext bindingContext, out bool stopOnIndexNotFound, out IEnumerable<string> indexes) {
368             string indexKey = CreateSubPropertyName(bindingContext.ModelName, "index");
369             ValueProviderResult vpResult = bindingContext.ValueProvider.GetValue(indexKey);
370 
371             if (vpResult != null) {
372                 string[] indexesArray = vpResult.ConvertTo(typeof(string[])) as string[];
373                 if (indexesArray != null) {
374                     stopOnIndexNotFound = false;
375                     indexes = indexesArray;
376                     return;
377                 }
378             }
379 
380             // just use a simple zero-based system
381             stopOnIndexNotFound = true;
382             indexes = GetZeroBasedIndexes();
383         }
384 
GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)385         protected virtual PropertyDescriptorCollection GetModelProperties(ControllerContext controllerContext, ModelBindingContext bindingContext) {
386             return GetTypeDescriptor(controllerContext, bindingContext).GetProperties();
387         }
388 
GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)389         protected virtual object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) {
390             object value = propertyBinder.BindModel(controllerContext, bindingContext);
391 
392             if (bindingContext.ModelMetadata.ConvertEmptyStringToNull && Object.Equals(value, String.Empty)) {
393                 return null;
394             }
395 
396             return value;
397         }
398 
GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext)399         protected virtual ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext) {
400             return TypeDescriptorHelper.Get(bindingContext.ModelType);
401         }
402 
403         // If the user specified a ResourceClassKey try to load the resource they specified.
404         // If the class key is invalid, an exception will be thrown.
405         // If the class key is valid but the resource is not found, it returns null, in which
406         // case it will fall back to the MVC default error message.
GetUserResourceString(ControllerContext controllerContext, string resourceName)407         private static string GetUserResourceString(ControllerContext controllerContext, string resourceName) {
408             string result = null;
409 
410             if (!String.IsNullOrEmpty(ResourceClassKey) && (controllerContext != null) && (controllerContext.HttpContext != null)) {
411                 result = controllerContext.HttpContext.GetGlobalResourceObject(ResourceClassKey, resourceName, CultureInfo.CurrentUICulture) as string;
412             }
413 
414             return result;
415         }
416 
GetValueInvalidResource(ControllerContext controllerContext)417         private static string GetValueInvalidResource(ControllerContext controllerContext) {
418             return GetUserResourceString(controllerContext, "PropertyValueInvalid") ?? MvcResources.DefaultModelBinder_ValueInvalid;
419         }
420 
GetValueRequiredResource(ControllerContext controllerContext)421         private static string GetValueRequiredResource(ControllerContext controllerContext) {
422             return GetUserResourceString(controllerContext, "PropertyValueRequired") ?? MvcResources.DefaultModelBinder_ValueRequired;
423         }
424 
GetZeroBasedIndexes()425         private static IEnumerable<string> GetZeroBasedIndexes() {
426             for (int i = 0; ; i++) {
427                 yield return i.ToString(CultureInfo.InvariantCulture);
428             }
429         }
430 
IsModelValid(ModelBindingContext bindingContext)431         protected static bool IsModelValid(ModelBindingContext bindingContext) {
432             if (bindingContext == null) {
433                 throw new ArgumentNullException("bindingContext");
434             }
435             if (String.IsNullOrEmpty(bindingContext.ModelName)) {
436                 return bindingContext.ModelState.IsValid;
437             }
438             return bindingContext.ModelState.IsValidField(bindingContext.ModelName);
439         }
440 
OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)441         protected virtual void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
442             Dictionary<string, bool> startedValid = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
443 
444             foreach (ModelValidationResult validationResult in ModelValidator.GetModelValidator(bindingContext.ModelMetadata, controllerContext).Validate(null)) {
445                 string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName);
446 
447                 if (!startedValid.ContainsKey(subPropertyName)) {
448                     startedValid[subPropertyName] = bindingContext.ModelState.IsValidField(subPropertyName);
449                 }
450 
451                 if (startedValid[subPropertyName]) {
452                     bindingContext.ModelState.AddModelError(subPropertyName, validationResult.Message);
453                 }
454             }
455         }
456 
OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext)457         protected virtual bool OnModelUpdating(ControllerContext controllerContext, ModelBindingContext bindingContext) {
458             // default implementation does nothing
459             return true;
460         }
461 
OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)462         protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
463             // default implementation does nothing
464         }
465 
OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)466         protected virtual bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
467             // default implementation does nothing
468             return true;
469         }
470 
471         [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")]
SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)472         protected virtual void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) {
473 
474             ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
475             propertyMetadata.Model = value;
476             string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);
477 
478             // If the value is null, and the validation system can find a Required validator for
479             // us, we'd prefer to run it before we attempt to set the value; otherwise, property
480             // setters which throw on null (f.e., Entity Framework properties which are backed by
481             // non-nullable strings in the DB) will get their error message in ahead of us.
482             //
483             // We are effectively using the special validator -- Required -- as a helper to the
484             // binding system, which is why this code is here instead of in the Validating/Validated
485             // methods, which are really the old-school validation hooks.
486             if (value == null && bindingContext.ModelState.IsValidField(modelStateKey)) {
487                 ModelValidator requiredValidator = ModelValidatorProviders.Providers.GetValidators(propertyMetadata, controllerContext).Where(v => v.IsRequired).FirstOrDefault();
488                 if (requiredValidator != null) {
489                     foreach (ModelValidationResult validationResult in requiredValidator.Validate(bindingContext.Model)) {
490                         bindingContext.ModelState.AddModelError(modelStateKey, validationResult.Message);
491                     }
492                 }
493             }
494 
495             bool isNullValueOnNonNullableType =
496                 value == null &&
497                 !TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType);
498 
499             // Try to set a value into the property unless we know it will fail (read-only
500             // properties and null values with non-nullable types)
501             if (!propertyDescriptor.IsReadOnly && !isNullValueOnNonNullableType) {
502                 try {
503                     propertyDescriptor.SetValue(bindingContext.Model, value);
504                 }
505                 catch (Exception ex) {
506                     // Only add if we're not already invalid
507                     if (bindingContext.ModelState.IsValidField(modelStateKey)) {
508                         bindingContext.ModelState.AddModelError(modelStateKey, ex);
509                     }
510                 }
511             }
512 
513             // Last chance for an error on null values with non-nullable types, we'll use
514             // the default "A value is required." message.
515             if (isNullValueOnNonNullableType && bindingContext.ModelState.IsValidField(modelStateKey)) {
516                 bindingContext.ModelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext));
517             }
518         }
519 
ShouldPerformRequestValidation(ControllerContext controllerContext, ModelBindingContext bindingContext)520         private static bool ShouldPerformRequestValidation(ControllerContext controllerContext, ModelBindingContext bindingContext) {
521             if (controllerContext == null || controllerContext.Controller == null || bindingContext == null || bindingContext.ModelMetadata == null) {
522                 // To make unit testing easier, if the caller hasn't specified enough contextual information we just default
523                 // to always pulling the data from a collection that goes through request validation.
524                 return true;
525             }
526 
527             // We should perform request validation only if both the controller and the model ask for it. This is the
528             // default behavior for both. If either the controller (via [ValidateInput(false)]) or the model (via [AllowHtml])
529             // opts out, we don't validate.
530             return (controllerContext.Controller.ValidateRequest && bindingContext.ModelMetadata.RequestValidationEnabled);
531         }
532 
ShouldUpdateProperty(PropertyDescriptor property, Predicate<string> propertyFilter)533         private static bool ShouldUpdateProperty(PropertyDescriptor property, Predicate<string> propertyFilter) {
534             if (property.IsReadOnly && !CanUpdateReadonlyTypedReference(property.PropertyType)) {
535                 return false;
536             }
537 
538             // if this property is rejected by the filter, move on
539             if (!propertyFilter(property.Name)) {
540                 return false;
541             }
542 
543             // otherwise, allow
544             return true;
545         }
546 
UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType)547         internal object UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType) {
548             bool stopOnIndexNotFound;
549             IEnumerable<string> indexes;
550             GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes);
551             IModelBinder elementBinder = Binders.GetBinder(elementType);
552 
553             // build up a list of items from the request
554             List<object> modelList = new List<object>();
555             foreach (string currentIndex in indexes) {
556                 string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
557                 if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey)) {
558                     if (stopOnIndexNotFound) {
559                         // we ran out of elements to pull
560                         break;
561                     }
562                     else {
563                         continue;
564                     }
565                 }
566 
567                 ModelBindingContext innerContext = new ModelBindingContext() {
568                     ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, elementType),
569                     ModelName = subIndexKey,
570                     ModelState = bindingContext.ModelState,
571                     PropertyFilter = bindingContext.PropertyFilter,
572                     ValueProvider = bindingContext.ValueProvider
573                 };
574                 object thisElement = elementBinder.BindModel(controllerContext, innerContext);
575 
576                 // we need to merge model errors up
577                 AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);
578                 modelList.Add(thisElement);
579             }
580 
581             // if there weren't any elements at all in the request, just return
582             if (modelList.Count == 0) {
583                 return null;
584             }
585 
586             // replace the original collection
587             object collection = bindingContext.Model;
588             CollectionHelpers.ReplaceCollection(elementType, collection, modelList);
589             return collection;
590         }
591 
UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType)592         internal object UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType) {
593             bool stopOnIndexNotFound;
594             IEnumerable<string> indexes;
595             GetIndexes(bindingContext, out stopOnIndexNotFound, out indexes);
596 
597             IModelBinder keyBinder = Binders.GetBinder(keyType);
598             IModelBinder valueBinder = Binders.GetBinder(valueType);
599 
600             // build up a list of items from the request
601             List<KeyValuePair<object, object>> modelList = new List<KeyValuePair<object, object>>();
602             foreach (string currentIndex in indexes) {
603                 string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
604                 string keyFieldKey = CreateSubPropertyName(subIndexKey, "key");
605                 string valueFieldKey = CreateSubPropertyName(subIndexKey, "value");
606 
607                 if (!(bindingContext.ValueProvider.ContainsPrefix(keyFieldKey) && bindingContext.ValueProvider.ContainsPrefix(valueFieldKey))) {
608                     if (stopOnIndexNotFound) {
609                         // we ran out of elements to pull
610                         break;
611                     }
612                     else {
613                         continue;
614                     }
615                 }
616 
617                 // bind the key
618                 ModelBindingContext keyBindingContext = new ModelBindingContext() {
619                     ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, keyType),
620                     ModelName = keyFieldKey,
621                     ModelState = bindingContext.ModelState,
622                     ValueProvider = bindingContext.ValueProvider
623                 };
624                 object thisKey = keyBinder.BindModel(controllerContext, keyBindingContext);
625 
626                 // we need to merge model errors up
627                 AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, keyFieldKey, keyType, thisKey);
628                 if (!keyType.IsInstanceOfType(thisKey)) {
629                     // we can't add an invalid key, so just move on
630                     continue;
631                 }
632 
633                 // bind the value
634                 ModelBindingContext valueBindingContext = new ModelBindingContext() {
635                     ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, valueType),
636                     ModelName = valueFieldKey,
637                     ModelState = bindingContext.ModelState,
638                     PropertyFilter = bindingContext.PropertyFilter,
639                     ValueProvider = bindingContext.ValueProvider
640                 };
641                 object thisValue = valueBinder.BindModel(controllerContext, valueBindingContext);
642 
643                 // we need to merge model errors up
644                 AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, valueFieldKey, valueType, thisValue);
645                 KeyValuePair<object, object> kvp = new KeyValuePair<object, object>(thisKey, thisValue);
646                 modelList.Add(kvp);
647             }
648 
649             // if there weren't any elements at all in the request, just return
650             if (modelList.Count == 0) {
651                 return null;
652             }
653 
654             // replace the original collection
655             object dictionary = bindingContext.Model;
656             CollectionHelpers.ReplaceDictionary(keyType, valueType, dictionary, modelList);
657             return dictionary;
658         }
659 
660         // This helper type is used because we're working with strongly-typed collections, but we don't know the Ts
661         // ahead of time. By using the generic methods below, we can consolidate the collection-specific code in a
662         // single helper type rather than having reflection-based calls spread throughout the DefaultModelBinder type.
663         // There is a single point of entry to each of the methods below, so they're fairly simple to maintain.
664 
665         private static class CollectionHelpers {
666 
667             private static readonly MethodInfo _replaceCollectionMethod = typeof(CollectionHelpers).GetMethod("ReplaceCollectionImpl", BindingFlags.Static | BindingFlags.NonPublic);
668             private static readonly MethodInfo _replaceDictionaryMethod = typeof(CollectionHelpers).GetMethod("ReplaceDictionaryImpl", BindingFlags.Static | BindingFlags.NonPublic);
669 
670             [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
ReplaceCollection(Type collectionType, object collection, object newContents)671             public static void ReplaceCollection(Type collectionType, object collection, object newContents) {
672                 MethodInfo targetMethod = _replaceCollectionMethod.MakeGenericMethod(collectionType);
673                 targetMethod.Invoke(null, new object[] { collection, newContents });
674             }
675 
ReplaceCollectionImpl(ICollection<T> collection, IEnumerable newContents)676             private static void ReplaceCollectionImpl<T>(ICollection<T> collection, IEnumerable newContents) {
677                 collection.Clear();
678                 if (newContents != null) {
679                     foreach (object item in newContents) {
680                         // if the item was not a T, some conversion failed. the error message will be propagated,
681                         // but in the meanwhile we need to make a placeholder element in the array.
682                         T castItem = (item is T) ? (T)item : default(T);
683                         collection.Add(castItem);
684                     }
685                 }
686             }
687 
688             [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents)689             public static void ReplaceDictionary(Type keyType, Type valueType, object dictionary, object newContents) {
690                 MethodInfo targetMethod = _replaceDictionaryMethod.MakeGenericMethod(keyType, valueType);
691                 targetMethod.Invoke(null, new object[] { dictionary, newContents });
692             }
693 
ReplaceDictionaryImpl(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<object, object>> newContents)694             private static void ReplaceDictionaryImpl<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<object, object>> newContents) {
695                 dictionary.Clear();
696                 foreach (KeyValuePair<object, object> item in newContents) {
697                     // if the item was not a T, some conversion failed. the error message will be propagated,
698                     // but in the meanwhile we need to make a placeholder element in the dictionary.
699                     TKey castKey = (TKey)item.Key; // this cast shouldn't fail
700                     TValue castValue = (item.Value is TValue) ? (TValue)item.Value : default(TValue);
701                     dictionary[castKey] = castValue;
702                 }
703             }
704         }
705     }
706 }
707