The first time I saw data annotations in action, it was love at first sight. The second time, it was strong passion. It didn't take much, though, for reason to regain control over emotion. At that point, data annotations became again just another technology with its own set of pros and cons -- like many technologies, it shines in a particular context but not necessarily in others.
Data annotations appeared for the first time in a beta of Silverlight 3 a few years ago. A lot has changed since then, and much work has been done to integrate data annotations in ASP.NET MVC. Today, the technology is flexible and extensible enough that you can use it in nearly any input-validation scenario. That doesn't mean, however, that data annotations are the ideal solution for every ASP.NET MVC input form. In this article, I'll present three input scenarios that show both the flexibility of data annotations and also their underlying nature, so that you can intelligently decide when they are appropriate for your use. Another tutorial about ASP.NET MVC 3 you can find here.
Property-Based Validation
When you present users an input form, you want them to enter single values into input fields. Some fields in the form may be required, and others may be optional. Some input fields expect to receive a date, whereas others can only accept numbers in a given range. You may also have input fields whose content must match a regular expression and other fields that employ a custom logic to determine whether or not the provided value is acceptable. The first scenario I consider is when input form validation occurs on individual values.
For the sake of our demonstration, let's assume we have a class like that shown in Figure below, which describes a tennis match.
public class NewMatchModel
{
[Required]
public String Player1 { get; set; }
[Required]
public String Player2 { get; set; }
[Range(0, 7)]
public Int32 FirstSet1 { get; set; }
[Range(0, 7)]
public Int32 FirstSet2 { get; set; }
[Range(0, 7)]
public Int32 SecondSet1 { get; set; }
[Range(0, 7)]
public Int32 SecondSet2 { get; set; }
[Range(0, 7)]
public Int32 ThirdSet1 { get; set; }
[Range(0, 7)]
public Int32 ThirdSet2 { get; set; }
}
The class has two string fields to contain the players' names and six numeric fields to contain the score that each player may achieve in up to three sets. Taken individually, each score field can have only an integer value ranging from 0 to 7. (Admittedly, I'm minimizing the complexity of tennis scoring in this example, but as you can see it is adequate for showing the limitation of property-based validation in some scenarios.)
At first glance, this class does a good job because it ensures that player names are always specified and each player's score can be from 0 to 7 (games per set). Validation applied to individual properties, however, is largely insufficient in this specific case. Figure 2 shows a form that passes validation but represents an invalid and unacceptable input for the application back end.
Whether you're simply creating a new record in a database table or processing that input to update statistics, the input you get is invalid for the domain logic. In particular, sets are played incrementally, meaning that to have nonzero values in, say, the third set, you must have completed the previous two. A set is completed when one of the players reaches 6 or 7 with at least a two-game lead. (Again, real logic is much more sophisticated, but this logic is sufficient for our validation purposes.)
The bottom line is that data annotations make it easy to arrange per-property validation, which is insufficient in many real-world scenarios.
I'll discuss cross-property validation using data annotations shortly, but before I do so, I will briefly address some common objections that I often receive at this point when I teach my ASP.NET MVC classes.
Extensibility of Data Annotations
One could say that per-property validation is still effective to avoid patently invalid data on the client. In ASP.NET MVC, data annotations can be easily arranged to emit JavaScript code that validates on the client. There's not really much you need to do -- just check that client-side validation is enabled in the web.config file and that the correct script files are on the site. If you start from the default ASP.NET MVC template, everything is simply in place. Client-side validation, though, is only one aspect of validation. From a business perspective, in fact, the data you get as shown in Figure 2 is invalid, and you must be able to detect this. I see two possible strategies for doing so.
One strategy is to use data annotations only for user interface validation and arrange another layer of validation in the domain model. In the second strategy, you take full advantage of data annotation's capabilities and use a single validation model that works on the client and server. For the latter strategy to work, though, you should recognize when per-property validation is not appropriate businesswise.
Data annotations comprise a core set of attributes but encourage you to create custom attributes that
implement custom validation logic. Before .NET Framework 4 and ASP.NET MVC 3, the implementation of data annotations didn't allow you to arrange cross-property validation easily. Thankfully, with .NET Framework 4, cross-property validation is easier.
Cross-Property Validation
Cross-property validation refers to a situation in which the content of a property cannot be properly validated without looking into the value stored in other properties. Cross-property validation likely requires a bit of context- and domain-specific code. For this reason, cross-property validation requires a custom attribute that turns out to be not really reusable outside of a given domain. As an example, consider the custom attribute in the figure below. The attribute is called SetScoreAttribute and is designed to validate the two input values forming the score of a given set.
public class SetScoreAttribute : ValidationAttribute
{
public SetScoreAttribute(String otherPropertyName)
: base("Invalid set score")
{
OtherPropertyName = otherPropertyName;
}
public String OtherPropertyName { get; set; }
protected override ValidationResult IsValid(Object value,
ValidationContext validationContext)
{
var otherPropertyInfo = validationContext.ObjectType.GetProperty(OtherPropertyName);
var otherSetScore = (Int32)otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
var thisSetScore = (Int32)value;
// Perform a very minimal validation: much more would be required realistically
var success = true;
var maxScore = Math.Max(thisSetScore, otherSetScore);
var minScore = Math.Min(thisSetScore, otherSetScore);
if (maxScore > 7 && minScore < 0)
success = false;
if (maxScore == otherSetScore && maxScore == 7)
success = false;
if (maxScore == 7 && minScore < 5)
success = false;
if (!success)
{
var message = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(message);
}
return null;
}
}
The attribute incorporates the effect of using the Range attribute on both fields and also ensures that, for example, scores such as 7-4 or 7-7 are detected as invalid. You use the SetScoreAttribute as in figure below.
public class New2MatchModel
{
[Required]
public String Player1 { get; set; }
[Required]
public String Player2 { get; set; }
[SetScoreAttribute("FirstSet2")]
public Int32 FirstSet1 { get; set; }
public Int32 FirstSet2 { get; set; }
[SetScoreAttribute("SecondSet2")]
public Int32 SecondSet1 { get; set; }
public Int32 SecondSet2 { get; set; }
[SetScoreAttribute("ThirdSet2")]
public Int32 ThirdSet1 { get; set; }
public Int32 ThirdSet2 { get; set; }
}
The attribute accepts as an argument the name of the related property (or properties) to take into account during validation. So you apply the SetScoreAttribute to, say, FirstSet1 and indicate FirstSet2 as the related property. In this way, the attribute can validate whether the score of the first set is consistent. Next, you reuse the attribute for the second and third set. It's a better solution, though not yet complete, as the figure below shows. As you can see, in fact, validation of individual sets works, but the score as a whole is still invalid.
Per-Class Validation
The lesson that these examples teach is that sometimes you need to proceed with a validation that applies at the class level, skipping over the apparent simplicity of validating property by property. In the specific example, we need to relate all six score properties and ensure that each individual property falls in the range 0-7, that two related properties describing a set fulfill other conditions, and that the score of the three sets forms a valid overall score for the match.
In .NET Framework 4 and ASP.NET MVC 3, each custom validation attribute can override the IsValid method, as the following example shows:
protected override ValidationResult IsValid(Object value, ValidationContext validationContext)
{
}
The ValidationContext type features a property named ObjectInstance, which references the instance being validated. By casting ObjectInstance to the type, you gain full access to all properties on the object you are going to validate.
protected override ValidationResult IsValid(Object value, ValidationContext validationContext)
{
var match = validationContext.ObjectInstance as NewMatchModel;
..
}
This gives you the flexibility and power to define a single validation attribute that, when applied at the class level (or to any property you like), can access the entire spectrum of values and check all the required business rules. In other words, you end up with something along the lines of the following code:
[TennisScoreAttribute]
public class New2MatchModel
{
[Required]
public String Player1 { get; set; }
[Required]
public String Player2 { get; set; }
..
}
You can still mix per-property and per-class validation, or you can incorporate all checks in a single attribute class. Note, though, that class-level validation is not triggered if some (decorated) properties are invalid. The figure below provides a demo of a class-level validation attribute.
public class OverallSetScoreAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(Object value, ValidationContext validationContext)
{
var match = validationContext.ObjectInstance as New2MatchModel;
if (match == null)
return null;
// Perform a very minimal validation
var success1 = ApplyMinimalSetValidation(match.FirstSet1, match.FirstSet2);
var success2 = ApplyMinimalSetValidation(match.SecondSet1,
match.SecondSet2);
var success3 = ApplyMinimalSetValidation(match.ThirdSet1, match.ThirdSet2);
if (!(success1 && success2 && success3))
{
var message = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(message);
}
return null;
}
private static Boolean ApplyMinimalSetValidation(Int32 set1, Int32 set2)
{
}
}
Clearly a class-level attribute needs to retain a reference to the type it validates, so unless you put a bit of effort into rewriting the code in Figure 6 to use reflection, the code is not reusable. On the other hand, if you don't much like the idea of writing a new attribute class every time, a better approach would be to use Microsoft's CustomValidation attribute at the class level. The attribute simply points to a method that performs the validation. The example in the figure below shows how to embed in the same class the logic for validation.
[CustomValidation(typeof(New2MatchModel), "ValidateMe"]
public class New2MatchModel
{
:
public static ValidationResult ValidateMe(New2MatchModel match)
{
:
}
}
It should be noted, though, that the first parameter of the CustomValidation attribute represents the validator type -- the type that contains the method you indicate as the second argument to the attribute (ValidateMe in the example in Figure above). What about the signature of ValidateMe? The method must be as follows:
public ValidationResult ValidateMe(Object value, ValidationContext context)
The first argument indicates the value of the property the attribute is attached to. If the CustomValidation attribute is attached to the class level, then the argument is a reference to the entire object. If the CustomValidation attribute applies at a single property, you get the value of the single property. In your definition of the method you can indicate a much more specific type than Object.
The IValidatableObject Interface
In the case of complex validation logic that spans over the entire class, you probably want to have a single place where all the rules are evaluated. A possible trick you could use to accomplish this consists of using a custom per-class attribute; another approach is based on using CustomValidation at the class level. In ASP.NET MVC 3, Microsoft also introduced the IValidatableObject interface for the same purpose, as the following example shows:
public interface IValidatableObject
{
IEnumerable<ValidationResult> Validate(ValidationContext context);
}
You basically implement the interface in your class and place the validation logic in the Validate method. Functionally speaking, this is equivalent to using a class-level attribute but probably makes for a cleaner solution.
The Way to Go
Validation is a matter of applying business rules. Sometimes validation can be done also on the client; at other times validation can be the same on the client and the server. Validation can even be a simple matter of applying basic rules to individual properties -- however, outside of tutorials or simple applications, this is almost never the case. So you're left to arrange your own validation layer around domain entities and/or view model objects. Per-class validation is the way to go; the details of how you implement per-class validation are up to you.