1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
|
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);
}
}
}
}
}
|