I recently came across an ASP.NET MVC issue at work where the validation for my Model was not firing correctly. The Model implemented the IValidatableObject
interface and thus the Validate
method which did some specific logic to ensure the state of the Model (the ModelState
). This Model also had some DataAnnotation
attributes on it to validate basic input.
Long story short, the issue I encountered was that when ModelState.IsValid == false
due to failure of the DataAnnotation
validation, the IValidatableObject.Validate
method is not fired, even though I needed it to be. This problem arose due to a rare situation in which ModeState.IsValid
was initially false but was later set to true in the Controller’s Action Method by some logic that removed errors from the ModelState
.
I did some research and learned that the DefaultModelBinder
of ASP.NET MVC short-circuits it’s logic: if the ModelState
is not valid (AKA is false), the IValidatableObject
logic that runs the Validate
method is never fired.
To thwart this, I created a custom Model Binder, a custom Model Binder Provider (to serve my custom Model Binder), and then registered the Model Binder Provider in the Application_Start
method of Global.asax.cs
. Here’s the code for a custom Model Binder that always fires the IValidatableObject.Validate
method, even if ModelState.IsValid == false
:
ForceValidationModelBinder:
/// <summary>
/// A custom model binder to force an IValidatableObject to execute
/// the Validate method, even when the ModelState is not valid.
/// </summary>
public class ForceValidationModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
ForceModelValidation(bindingContext);
}
private static void ForceModelValidation(ModelBindingContext bindingContext)
{
// Only run this code for an IValidatableObject model
IValidatableObject model = bindingContext.Model as IValidatableObject;
if(model == null)
{
// Nothing to do
return;
}
// Get the model state
ModelStateDictionary modelState = bindingContext.ModelState;
// Get the errors
IEnumerable<ValidationResult> errors = model.Validate(new ValidationContext(model, null, null));
// Define the keys and values of the model state
List<string> modelStateKeys = modelState.Keys.ToList();
List<ModelState> modelStateValues = modelState.Values.ToList();
foreach (ValidationResult error in errors)
{
// Account for errors that are not specific to a member name
List<string> errorMemberNames = error.MemberNames.ToList();
if (errorMemberNames.Count == 0)
{
// Add empty string for errors that are not specific to a member name
errorMemberNames.Add(string.Empty);
}
foreach (string memberName in errorMemberNames)
{
// Only add errors that haven't already been added.
// (This can happen if the model's Validate(...) method is called more than once, which will happen when there are no property-level validation failures)
int index = modelStateKeys.IndexOf(memberName);
// Try and find an already existing error in the model state
if(index == -1 || !modelStateValues[index].Errors.Any( i => i.ErrorMessage == error.ErrorMessage))
{
// Add error
modelState.AddModelError(memberName, error.ErrorMessage);
}
}
}
}
}
ForceValidationModelBinderProvider:
/// <summary>
/// A custom model binder provider to provide a binder that forces an IValidatableObject to execute the Validate method, even when the ModelState is not valid.
/// </summary>
public class ForceValidationModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
return new ForceValidationModelBinder();
}
}
Global.asax.cs:
protected void Application_Start()
{
// Register the force validation model binder provider
ModelBinderProviders.BinderProviders.Clear();
ModelBinderProviders.BinderProviders.Add(new ForceValidationModelBinderProvider());
}