During the last weeks I got some insights on ASP.NET MVC 2. Personally I like the programming model of MVC in contrast to WebForms, although the productivity first seems to be lower in common data-driven scenarios. One of the most advertised features of MVC 2 is the support of Data Annotations for adding validators and further information right to your model via attributes. Data Annotations have their origins in ASP.NET Dynamic Data and are defined in the System.ComponentModel.DataAnnotations
namespace that ships with .NET 4.0. Microsoft is promoting Data Annotations even further and beside MVC and Dynamic Data they can be used in Silverlight, too.
Data Annotations offer many capabilities for model validation to your application. There are predefined validators for common constraints: RangeAttribute
, RegularExpressionAttribute
, RequiredAttribute
and StringLengthAttribute
. Those attributes can be automatically checked on the client-side (via Javascript) as well. Furthermore there is a CustomValidationAttribute
which allows you to define custom validation logic. Personally I feel a bit ambivalent on Data Annotations. Beside of validation logic you are able to define UI-relevant information on your model which I can’t encourage if done on the core domain model. On the other side you are able to extend your model with Data Annotations on the UI layer… But I don’t want to discuss the usage of Data Annotations here. There are scenarios where they are a perfect match and there are other cases where you want to do validation on your own…
Simple example
If you use Data Annotations for validation you get great tool support in ASP.NET MVC 2 and other UI technologies. Imagine the following model class Product
:
public class Product { [Required(ErrorMessage="ProductID is a required field")] public int ProductID { get; set; } [Required(ErrorMessage = "ProductName is a required field")] [StringLength(40, ErrorMessage = "ProductName can only contain up to 40 characters")] public string ProductName { get; set; } [StringLength(20, ErrorMessage = "QuantityPerUnit can only contain up to 20 characters")] public string QuantityPerUnit { get; set; } [Range(0, (double)decimal.MaxValue, ErrorMessage = "UnitPrice must be a valid positive currency")] public decimal? UnitPrice { get; set; } }
Entities of this model class should be editable through the following strongly-typed view Edit.aspx
:
<%@ Page Title="Edit Product" Language="C#" Inherits="System.Web.Mvc.ViewPage<Product>" %> <h2>Edit Product</h2> <% using (Html.BeginForm()) {%> <%: Html.ValidationSummary(true) %> <fieldset style="padding:10px;"> <legend>Product Fields model.ProductName) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.ProductName) %> <%: Html.ValidationMessageFor(model => model.ProductName) %> </div> <div class="editor-label" style="margin-top:10px;"> <%: Html.LabelFor(model => model.QuantityPerUnit) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.QuantityPerUnit) %> <%: Html.ValidationMessageFor(model => model.QuantityPerUnit) %> </div> <div class="editor-label" style="margin-top:10px;"> <%: Html.LabelFor(model => model.UnitPrice) %> </div> <div class="editor-field"> <%: Html.TextBoxFor(model => model.UnitPrice, String.Format("{0:F}", Model.UnitPrice)) %> <%: Html.ValidationMessageFor(model => model.UnitPrice) %> </div> <p style="margin-top:10px;"> <input type="submit" value="Save" /> </p> </fieldset> <% } %>
When posting the form in this view the following action Edit()
on the ProductController
is invoked:
[HttpPost] public ActionResult Edit(Products product) { if (!ModelState.IsValid) return View(product); // ... }
And here’s where magic comes into play. When posting the form the Data Annotations on the Product
class are checked automatically. If there are any validation errors ModelState.IsValid
is set to false
and ModelState
itself will contain the error messages. Those error messages automatically are displayed in the UI through the Html.ValidationMessageFor()
helpers as shown below:
Everything’s fine?
Now you could think that everything’s fine with this solution, right? But that’s not the case! Data Annotations have an important and not very obvious shortcoming which can lead to serious data consistency problems. The problem: Data Annotations are checked during the model binding phase based on the posted form values and not on the bound model entity. That means only the Data Annotations on the posted form values are checked, but not other properties which are perhaps defined on the model class, but missing in the form values. Imagine for example some bad guy who visits your page and edits a Product. Before posting the form he manipulates the DOM of the page and removes some form values. Those values will not be checked on server-side and thus the (invalid) Product
will be saved to the DB or some operations will be done on it. Got the point?
So what can we do? We can manually check the defined validation Data Annotations in our controller action on the model class after the model binding procedure. Therefore we can for example develop a custom action filter attribute (inspired from here):
public class ModelValidationAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { // get the controller for access to the ModelState dictionary var controller = filterContext.Controller as Controller; if(controller != null) { var modelState = controller.ModelState; // get entities that could have validation attributes foreach (var entity in filterContext.ActionParameters.Values.Where(o => o != null)) { // get metadata attribute MetadataTypeAttribute metadataTypeAttribute = entity.GetType().GetCustomAttributes(typeof(MetadataTypeAttribute), true) .FirstOrDefault() as MetadataTypeAttribute; Type attributedType = (metadataTypeAttribute != null) ? metadataTypeAttribute.MetadataClassType : entity.GetType(); // get all properties of entity class and possibly defined metadata class var attributedTypeProperties = TypeDescriptor.GetProperties(attributedType) .Cast<PropertyDescriptor>(); var entityProperties = TypeDescriptor.GetProperties(entity.GetType()) .Cast<PropertyDescriptor>(); // get errors from all validation attributes of entity and metadata class var errors = from attributedTypeProperty in attributedTypeProperties join entityProperty in entityProperties on attributedTypeProperty.Name equals entityProperty.Name from attribute in attributedTypeProperty.Attributes.OfType<ValidationAttribute>() where !attribute.IsValid(entityProperty.GetValue(entity)) select new KeyValuePair<string, string>(attributedTypeProperty.Name, attribute.FormatErrorMessage(string.Empty)); // add errors to ModelState dictionary foreach (var error in errors) if (!modelState.ContainsKey(error.Key)) modelState.AddModelError(error.Key, error.Value); } } base.OnActionExecuting(filterContext); } }
This attribute takes all validation attributes into account which are defined on the model class itself or on an associated metadata class. The attribute adds all validation errors to the ModelState
dictionary of the controller which have not been added before and which are defined in the ErrorMessage or the Error Resource of the validation attribute. Note that your controller has to derive from the Controller
class and not from ControllerBase
, but that’s the default setting and should not be a problem.
Now we only have to add this attribute to our controller action and validation will be done on the bound model class:
[HttpPost] [ModelValidation] public ActionResult EditProduct(Products product) { if (!ModelState.IsValid) return View(product); // ... }
Conclusion
This post has shown how you can check defined validation Data Annotations from your code and why it’s important in ASP.NET MVC 2 to do so. Of course you don’t have to define a custom action filter attribute for this task and can do this task by a custom validation helper in your business logic if you like. Once again this story told me one thing: don’t rely on technical solutions and question their background behavior all the time…