Skip to content

ASP.NET MVC Quick Tip: Check Data Annotations from code

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:
ASP.NET MVC 2 - Validation with Data Annotations

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…

kick it on DotNetKicks.com

{ 6 } Comments

  1. JoeReynolds | 2010/05/25 at 19:34 | Permalink

    I’ve implemented this and all my post code still works as before. That’s good, but is there some way to test it? Also, my controllers all inherit from a base controller so perhaps the code is doing nothing.

  2. MatthiasJ | 2010/05/25 at 20:14 | Permalink

    Of course you can test itby invoking a controller action with model entities you assume. Then you can look if the model state is valid or invalid depending on what you expect to get from your annotated model. Base controller should be no problem if the concrete controller still has the MVC “Controller” class in the inheritance hierarchy.

  3. JoeReynolds | 2010/05/25 at 21:26 | Permalink

    Thanks for the prompt response. I’m not sure how to manipulate the model to produce a valid test.

    Assume a single input of “ArticleName” — — 35 varchar in database,
    and
    field input of

    Html.TextBoxFor(model => model.articleToCreate.ArticleName, new { style = “width: 235px;” })

    ArticleName has a Required attribute in my Class.

    How could this be tested?

  4. MatthiasJ | 2010/05/25 at 21:36 | Permalink

    Hi Joe,

    I’m not sure WHAT you want to test. Do you want to test that the model validation is performed and produces an invalid ModelState when the ArticleName is null/empty? Of course your test has to invoke the according controller action with the invalid model entity. Afterwards you can check if the ModelState was invalid (thus the model validation was performed correctly). I hope you know how to test MVC controllers, else there are some great articles on the web.

  5. JoeReynolds | 2010/05/26 at 04:03 | Permalink

    If ArticleName does not allow NULL in db and the Required attribute is removed from the Class, then validation still returns an error message on the form when submitted. The error returned is:
    The value ” is invalid.
    However, this happens with or without using your code above.

    If ArticleName allows NULL then the item is added to db with a null value, and that can cause major problems unless SELECT commands include a ISNULL value.

    As to what I want to test, it is to test something live in a way some malicious user might do so as to see what the code you provided does.

    At any rate, I don’t want to impose on your time and thanks for your replies.

  6. Charles Boyung | 2010/06/17 at 19:32 | Permalink

    Your entire last section is wholly unnecessary (as far back as early February with MVC2 RC2). You state: “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.”

    But MVC2 Data Annotation Validation now does Model Validation not Input Validation. Check out http://weblogs.asp.net/scottgu/archive/2010/02/05/asp-net-mvc-2-release-candidate-2-now-available.aspx

{ 1 } Trackback

  1. […] such as the DataTypeAttribute or possibly new, custom attributes. A version of this code (from http://www.minddriven.de/index.php/technology/dot-net/web/asp-net-mvc/check-data-annotations-from-co&#8230;) works well to read the attributes once I have the EF Code First POCO object’s Type […]

Post a Comment

Your email is never published nor shared. Required fields are marked *