MrKWatkins

A Data Annotations Aware Model Binder

The System.ComponentModel.DataAnnotations assembly added with .NET 3.5 SP1 is quite useful. It contains various attributes that allow you to specify various constraints on the properties of your class. Ideal for model classes; constraints on the data are kept inside your model, nice and succinctly. However ASP.NET MVC version 1 does not contain out of the box server side support for them. Version 2 will but I can't wait that long... Whilst you can use something like the excellent xVal for client side validation of data annotations being a good programmer you'll also want to have the same validation server side in case someone turns off JavaScript in their browser.

Sadly I haven't been able to find a good example of how to do this. The examples I have found tend to require manual calls to some method to perform the validation, such as this one in Steve Sanderson's blog about how to use xVal. I don't want to have to do an extra manual call for my validation; I might forget! The ideal would be a model binder that is aware of the data annotations; I could set it as the default binder and forget about it. The only example of a model binder I could find was in the ASP.NET site on CodePlex but that required a newer version of the DataAnnotations assembly (basically the .NET 4.0 one) which seemed like overkill to me. How hard can it be to write one?

Turns out not very... All you have to do is:

  1. Create a class that inherits from DefaultModelBinder.
  2. Override the OnPropertyValidating method. Your version should call the base method to get the errors from the default binder then look for any ValidationAttributes on the property, check if there are errors by calling the IsValid method and add them to the ModelState if there are. Something like this:
    protected override bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
    {
    	// Run everything by the default implementation first before checking data annotations.
    	var isValid = base.OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, value);
    	
    	// Loop through any validation attributes.
    	foreach (var validationAttribute in propertyDescriptor.Attributes.OfType<ValidationAttribute>())
    	{
    		// Are we valid?
    		if (!validationAttribute.IsValid(value))
    		{
    			// No. Let's add the error to the model state and set our isValid status to false.
    			string key = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
    			
    			// Add the error to the model state.
    			bindingContext.ModelState.AddModelError(key, validationAttribute.FormatErrorMessage(propertyDescriptor.DisplayName)));
    			isValid = false;
    		}
    	}
    	return isValid;
    }
  3. Replace the default model binder with an instance of your new one via the ModelBinders.Binders.DefaultBinder property.

Okay, it's not quite as simple as that. You also have to take into account any MetadataTypeAttributes that might be found on your model class. This attribute allows you to specify another type to put your metadata on. You can specify identical properties on this other type and decorate them with the DataAnnotations attributes instead. Pointless? Well yes, it should be really; why not just decorate your properties? However it comes in useful if you're working with auto-generated code that doesn't allow you to add attributes, such as the evil and crappy LINQ to SQL designer. By using the MetadataTypeAttribute you can create a partial version of your LINQ to SQL entity class, add the attribute to that and specify constraints for the auto generated properties in some other class. (Whether you should be using LINQ to SQL entities as your model is an ethical debate I am going nowhere near)

If you can't be bothered to do all this of course then please feel free to download one I made earlier. This binder takes into account MetadataTypeAttributes and I've even thrown in a demo project so you can see the thing in action. Enjoy!