How to Avoid Cut and Paste Code with ASP.NET MVC 2 Model Validation
In this post, I will demonstrate how to make your own model validation attributes in order to share common validations throughout an ASP.NET MVC application, and which support MVC 2's client-side validation feature.
As an example, consider a model for an address.
Now I would like to add a validation attribute in order to ensure that only valid ZIP codes can be entered. In the US, a valid ZIP code is either five digits, or five digits and then a hyphen and then four digits, e.g. 43082 or 43082-6999. I can use the built-in RegularExpressionAttribute for this:
This is enough to give me server-side validation when a model of this type is bound by the MVC framework. If I turn on client-side validation, then JavaScript will be generated to do the same validation on the client, as well.
So far, so good.
If this is the only ZIP code which ever appears in my application, I'm done. But if I have several ZIP codes in the application, I would like to consolidate this code into one place.
Perhaps, in a future release of the application, I will support Canadian postal codes in addition to US ZIP codes. I would like to be able to make this change in one place.
I can create a dedicated ZIP code validation attribute by subtyping the RegularExpressionAttribute.
...and update the edit model to use it:
This looks good, and seems to work, but testing will reveal that MVC 2's client-side validation feature has stopped working, even though server-side validation will continue to work as expected. Examining the rendered JavaScript will show that the expected regular expression has not been injected into the page.
Internally, MVC stores a dictionary of attributes, and their corresponding client validation adapters. When it comes time to render the page, it goes through the list of attributes, and finds corresponding entries in the dictionary. Because of the way that this is implemented, it will not find adapters for subtypes of known attribute types. This seems like a violation of the Liskov Substitution Principle.
We can work around the problem, though. There is an API for adding additional attributes to the internal dictionary. In Global.asax.cs, you can add the following code to the
The only comprehensive "documentation" for this that I'm aware of is the MVC source code.
After adding this code, client-side validation will start working again.
Validating a ZIP Code
As an example, consider a model for an address.
public class EditModel
{
public Guid Id { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZIP { get; set; }
}
Now I would like to add a validation attribute in order to ensure that only valid ZIP codes can be entered. In the US, a valid ZIP code is either five digits, or five digits and then a hyphen and then four digits, e.g. 43082 or 43082-6999. I can use the built-in RegularExpressionAttribute for this:
public class EditModel
{
public Guid Id { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
[RegularExpression(@"\d{5}(-\d{4})?", ErrorMessageResourceName = "InvalidZIPCode", ErrorMessageResourceType = typeof(MyApp.Resources))]
public string ZIP { get; set; }
}
This is enough to give me server-side validation when a model of this type is bound by the MVC framework. If I turn on client-side validation, then JavaScript will be generated to do the same validation on the client, as well.
//...
"ValidationParameters":{"pattern":"\\d{5}(-\\d{4})?"},"ValidationType":"regularExpression"}
// ..
So far, so good.
Don't Repeat Yourself
If this is the only ZIP code which ever appears in my application, I'm done. But if I have several ZIP codes in the application, I would like to consolidate this code into one place.
Perhaps, in a future release of the application, I will support Canadian postal codes in addition to US ZIP codes. I would like to be able to make this change in one place.
I can create a dedicated ZIP code validation attribute by subtyping the RegularExpressionAttribute.
[AttributeUsageAttribute(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class ZipCodeAttribute : RegularExpressionAttribute
{
public ZipCodeAttribute()
: base(@"\d{5}(-\d{4})?")
{
ErrorMessageResourceName = "InvalidZIPCode";
ErrorMessageResourceType = typeof(MyApp.Resources);
}
}
...and update the edit model to use it:
public class EditModel
{
public Guid Id { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
[ZipCode]
public string ZIP { get; set; }
}
Fixing Client-Side Validation
This looks good, and seems to work, but testing will reveal that MVC 2's client-side validation feature has stopped working, even though server-side validation will continue to work as expected. Examining the rendered JavaScript will show that the expected regular expression has not been injected into the page.
Internally, MVC stores a dictionary of attributes, and their corresponding client validation adapters. When it comes time to render the page, it goes through the list of attributes, and finds corresponding entries in the dictionary. Because of the way that this is implemented, it will not find adapters for subtypes of known attribute types. This seems like a violation of the Liskov Substitution Principle.
We can work around the problem, though. There is an API for adding additional attributes to the internal dictionary. In Global.asax.cs, you can add the following code to the
Application_Start
event handler:
private void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
// ...
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(ZipCodeAttribute), typeof(RegularExpressionAttributeAdapter));
The only comprehensive "documentation" for this that I'm aware of is the MVC source code.
After adding this code, client-side validation will start working again.

Very cool. The first part is straightforward, but the second part would have absolutely killed me if I hadn't read this post