In .NET 3.5 SP1, the ASP.NET team introduced a new DLL named System.ComponentModel.DataAnnotations, in conjunction with the ASP.NET Dynamic Data project. The purpose of this DLL is to provide UI-agnostic ways of annotating your data models with semantic attributes like [Required] and [Range]. Dynamic Data uses these attributes, when applied to your models, to automatically wire up to validators in WebForms. The UI-agnostic bit is important, and is why the functionality exists in the System.ComponentModel.DataAnnotations namespace, rather than somewhere under System.Web.
For .NET 4.0, the .NET RIA Services team is also supporting DataAnnotations (which have been significantly enhanced since their initial introduction). This means that models you annotate can end up with automatic validation being performed in both client- and server-side code, supporting WebForms (via Dynamic Data) as well as Silverlight (via RIA Services).
In our exploration of data support in ASP.NET MVC, we wrote a model binder which does server-side validation in MVC by relying on the DataAnnotations attributes. Using a preview of the .NET 4.0 DataAnnotations DLL (the same one that we released with Dynamic Data 4.0 Preview 3), we extended the default model binder behavior to include DataAnnotations support, and then released the code as a sample project with unit tests.
How Does It Work?
The MVC DefaultModelBinder class has a lot of extensibility points, some of which are designed specifically with validation in mind. The DataAnnotations model binder leverages those extension points to allow DataAnnotations attributes to contribute to the validation of a model.
For example, let’s take a simple model:
public class Contact { public string FirstName { get; set; } public string LastName { get; set; } }
In a standard MVC application, if I want FirstName and LastName to be required, I have to write custom validation code to make this happen. My action method might look something like this:
public ActionResult Edit(Contact contact) { if (String.IsNullOrEmpty(contact.FirstName)) ModelState.AddModelError("FirstName", "First name is required"); if (String.IsNullOrEmpty(contact.LastName)) ModelState.AddModelError("LastName", "Last name is required"); try { if (ModelState.IsValid) { // Submit the changes to the database here return Redirect("Index"); } } catch(Exception ex) { // Log the exception somewhere to be looked at later ModelState.AddModelError("*", "An unexpected error occurred."); } return View(contact); }
Now let’s take a look at the same model, but using DataAnnotations:
public class Contact { [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } }
And our updated action method:
public ActionResult Edit(Contact contact) { try { if (ModelState.IsValid) { // Submit the changes to the database here return Redirect("Index"); } } catch(Exception ex) { // Log the exception somewhere to be looked at later ModelState.AddModelError("*", "An unexpected error occurred."); } return View(contact); }
Notice how much cleaner the action method is, now that the validation of the model has been moved into the metadata on the model itself. Now the action method can just focus on submission and error handling, without being concerned about how to validate the model. Score one for separation of concerns! :)
To make this work, you need to compile the DataAnnotations model binder project, and then added references to the two DLLs in you find in the src\bin\Debug folder (Microsoft.Web.Mvc.ModelBinders.dll and System.ComponentModel.DataAnnotations.dll).
Then, in your Global.asax.cs file, you make the following changes to register the model binder:
void Application_Start() { RegisterRoutes(RouteTable.Routes); RegisterModelBinders(ModelBinders.Binders); // Add this line } public void RegisterModelBinders(ModelBinderDictionary binders) // Add this whole method { binders.DefaultBinder = new Microsoft.Web.Mvc.DataAnnotations.DataAnnotationsModelBinder(); }
Now when you submit forms, the model binder will automatically find instances of the DataAnnotations attributes on your models and run the validations you’ve specified.
How Do I Test It?
Using the DataAnnotations attributes for your models moves the validation out of the controller actions and into the model binder, which means your unit tests for your controller actions will be simplified.
When you’re writing tests for this, you need to verify three things:
- Is the DataAnnotationsModelBinder registered as the default binder?
You’ll only do this once for the whole application, much like the route tests you would write. - Is my model properly decorated with DataAnnotations attributes?
You’ll end up writing tests for each validation attribute that you add to your model. - Does my action method properly react when the model state is invalid?
You’ll only need to write this once per action method.
Those tests will probably look something like this:
[Fact] public void Default_model_binder_is_DataAnnotationsModelBinder() { // Arrange var binders = new ModelBinderDictionary(); var application = new MvcApplication(); // Act application.RegisterModelBinders(binders); // Assert Assert.IsType<DataAnnotationsModelBinder>(binders.DefaultBinder); } [Fact] public void Contact_model_FirstName_is_required() { // Arrange var propertyInfo = typeof(Contact).GetProperty("FirstName"); // Act var attribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute)) .Cast<RequiredAttribute>() .FirstOrDefault(); // Assert Assert.NotNull(attribute); } [Fact] public void Contact_Edit_action_shows_view_with_invalid_ModelState() { // Arrange var contact = new Contact(); var controller = new ContactController(); controller.ModelState.AddModelError("*", "Invalid model state"); // Act var actionResult = controller.Edit(contact); // Assert var viewResult = Assert.IsType<ViewResult>(actionResult); Assert.Empty(viewResult.ViewName); Assert.Same(contact, viewResult.ViewData.Model); }
In your TDD rhythm, you’ll find yourself writing tests like #2 in advance of defining the models that will support those actions and views. Then you’ll find yourself writing tests like #3 in advance of adding new actions (that test obviously isn’t exhaustive, as it doesn’t test the valid model path nor the “throwing an exception” path, but you get the idea).
Where Did My “Validation” Tests Go?
One thing you’ll notice is that there is no test which explicitly says “given an empty first name, a model state error should occur”. The reason for that is simple: the DataAnnotations attributes behave in an AOP-style fashion where their behavior becomes visible only when the whole system is functioning.
You can consider the DataAnnotations model binder like an accepted piece of infrastructure in your project, just like you already do for the default model binder (or the action invoker, or the controller factory, or any of the dozens of other moving parts that makes an ASP.NET MVC application “go”).
Even so, you may still want to have tests which verify that an empty first name edit box, when submitted, returns back a validation error to the user.
The most common way to see the system running as a whole is to do exploratory testing. In this way, you start the application and try using the forms with the validation attributes, and observe the behavior to ensure that the validation is taking place. Many QA departments rely primarily on scripted exploratory testing to ensure that applications are functioning properly before deploying them into production.
An alternative that is popular with agile teams is automated acceptance testing, where developers, testers and customers collaborate to write tests which ensure the functioning of the system as a whole, using tools like the Lightweight Test Automation Framework. These tests allow customers to know that the application is functioning properly with a high degree of confidence, without the delays and manual labor involved with exploratory testing.
Happy validating!