In System.Web.Mvc version 5.2.3.0, the DefaultModelBinder
does perform validation, arguably violating separation of concerns, and there isn't a way to shut it off entirely via any setting or configuration. Other SO posts mention turning off the implicit required attribute for value types with the following line of code in your Global.asax.cs, Application_Start()
method...
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
See: https://stackoverflow.com/a/2224651 (which references a forum with an answer directly from the asp.net team).
However, that is not enough. All model class getters are still executed because of the code inside the DefaultModelBinder.BindProperty(...)
method. From the source code...
https://github.com/mono/aspnetwebstack/blob/master/src/System.Web.Mvc/DefaultModelBinder.cs
215 // call into the property's model binder
216 IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
217 object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
218 ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
219 propertyMetadata.Model = originalPropertyValue;
220 ModelBindingContext innerBindingContext = new ModelBindingContext()
221 {
222 ModelMetadata = propertyMetadata,
223 ModelName = fullPropertyKey,
224 ModelState = bindingContext.ModelState,
225 ValueProvider = bindingContext.ValueProvider
226 };
227 object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
Line 217 is the offender. It calls the getter prior to setting the value from the request (the ultimate purpose of this method), apparently so that it can pass the original value in the ModelBindingContext
parameter to the GetPropertyValue(...)
method on line 227. I could not find any reason for this.
I use calculated properties extensively in my model classes that certainly throw exceptions if the property expression relies on data that has not been previously set since that would indicate a bug elsewhere in the code. The DefaultModelBinder
behavior spoils that design.
To solve the problem in my case, I wrote a custom model binder that overrides the BindProperty(...)
method and removes the call to the getters. This code is just a copy of the original source, minus lines 217 and 219. I also removed lines 243 through 259 since I am not using model validation, and that code references a private method to which the derived class does not have access (another problematic design of the DefaultModelBinder.BindProperty(...)
method). Here is the custom model binder.
public class NoGetterModelBinder : DefaultModelBinder {
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey)) return;
IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
ModelBindingContext innerBindingContext = new ModelBindingContext() {
ModelMetadata = propertyMetadata,
ModelName = fullPropertyKey,
ModelState = bindingContext.ModelState,
ValueProvider = bindingContext.ValueProvider,
};
object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
propertyMetadata.Model = newPropertyValue;
ModelState modelState = bindingContext.ModelState[fullPropertyKey];
if (modelState == null || modelState.Errors.Count == 0) {
if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) {
SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
}
} else {
SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
}
}
}
You can place that class anywhere in your web project, I just put it in Global.asax.cs. Then, again in Global.asax.cs, in Application_Start()
, add the following line of code to make it the default model binder for all classes...
ModelBinders.Binders.DefaultBinder = new NoGetterModelBinder();
This will prevent getters from being called on your model classes.