The .NET Framework also provides us with an alternative, attribute-based validation system in the System.ComponentModel.DataAnnotations namespace. It is mostly comprised of a wide range of attribute classes that we can decorate our data Model properties with so as to specify our validation rules. In addition to these attributes, it also includes a few validation classes, which we will investigate later.
As an example, let's look at replicating the current validation rules from our ProductNotify class with these data annotation attributes. We need to corroborate the fact that the Name property is entered and has a length of 25 characters or less, and that the Price property is more than zero. For the Name property, we can use the RequiredAttribute and the MaxLengthAttribute attributes:
using System.ComponentModel.DataAnnotations;
...
[Required(ErrorMessage = "Please enter the product name.")] [MaxLength(25, ErrorMessage = "The product name cannot be longer than twenty-five characters.")] public string Name { get { return name; } set { if (name != value) { name = value; NotifyPropertyChangedAndValidate(); } } }
As with all attributes, we can omit the word Attribute when using them to decorate properties. Most of these data annotation attributes declare one or more constructors with a number of optional parameters. The ErrorMessage input parameter is used in each to set the message to output when the specified condition is not met.
The RequiredAttribute constructor has no input parameters and simply checks that the data bound value is not null or empty. The constructor of the MaxLengthAttribute class takes an integer that specifies the maximum allowable length of the data bound value and it will raise a ValidationError instance if the input value is longer.
For the Price property, we can make use of the RangeAttribute with a really high maximum value, as there is no MinimumAttribute class available:
[Range(0.01, (double)decimal.MaxValue, ErrorMessage = "Please enter a valid price for the product.")] public decimal Price { get { return price; } set { if (price != value) { price = value; NotifyPropertyChangedAndValidate(); } } }
The constructor of the RangeAttribute class takes two double values, which specify the minimum and maximum valid values, and, in this example, we set the minimum to one penny and the maximum to the maximum decimal value, as our Price property is of the decimal type. Note that we could not use the RequiredAttribute class here, as numeric data bound values will never be null or empty.
There are a large number of these data annotation attribute classes, covering the most common validation situations, but when we have a requirement that does not have a pre-existing attribute to help us, we can create our own custom attribute by extending the ValidationAttribute class. Let's create an attribute that only validates a minimum value:
using System.ComponentModel.DataAnnotations; namespace CompanyName.ApplicationName.DataModels.Attributes { public class MinimumAttribute : ValidationAttribute { private double minimumValue = 0.0; public MinimumAttribute(double minimumValue) { this.minimumValue = minimumValue; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (value.GetType() != typeof(decimal) || (decimal)value < (decimal)minimumValue) { string[] memberNames = new string[] { validationContext.MemberName }; return new ValidationResult(ErrorMessage, memberNames); } return ValidationResult.Success; } } }
When we extend the ValidationAttribute class, we only need to override the IsValid method to return true or false, depending on our input value, which is specified by the value input parameter. In our simple example, we first declare the minimumValue field to store the target minimum allowable value to use during validation.
We populate this field in the class constructor, with the value that users of our class provide. Next, we override the IsValid method that returns a ValidationResult instance. In this method, we first check the type of the value input parameter and then cast it to decimal, in order to compare it with the value of our minimumValue field.
Note that we have hardcoded this double type as the type of our minimum value, because although our Price property is decimal, the decimal type is not considered primitive and therefore cannot be used in an attribute. A better, more reusable solution, would be to declare a number of constructors that accept different numerical types that could be used in a wider range of situations and to update our IsValid method to be able to compare the different types with the input value.
In our example, if the input value is either the incorrect type, or the cast value is less than the value of the minimumValue field, we first create the memberNames variable and insert the value of the MemberName property from the validationContext input parameter. We then return a new instance of the ValidationResult class, inputting the used error message and our memberNames collection.
If the input value is valid according to our particular validation logic, then we simply return the ValidationResult.Success field to signify successful validation. Let's now look at our new attribute being used on the Price property of our ProductNotify class:
[Minimum(0.01, ErrorMessage = "Please enter a valid price for the product.")] public decimal Price { get { return price; } set { if (price != value) { price = value; NotifyPropertyChangedAndValidate(); } } }
In effect, our new attribute will work exactly as the previously used RangeAttribute instance, but it clearly demonstrates how we can create our own custom validation attributes. Before we move on to see how we can read these errors with our code, let's first see how we can access the value of a second property from the data Model in our attribute, as this is a common requirement when validating:
PropertyInfo propertyInfo = validationContext.ObjectType.GetProperty(otherPropertyName); if (propertyInfo == null) throw new ArgumentNullException( $"Unknown property: {otherPropertyName}"); object otherPropertyValue = propertyInfo.GetValue(validationContext.ObjectInstance);
This example assumes that we have added a reference to the System and System.Reflection namespaces and declared a string field named otherPropertyName, which is populated with the name of the other property name in the constructor. Using reflection, we attempt to access the PropertyInfo object that relates to the specified property name.
If the PropertyInfo object is null, we throw an ArgumentNullException object, alerting the developer that they have used a non-existent property name. Otherwise, we use the GetValue method of the PropertyInfo object to retrieve the value from the other property.
Now that we've seen how to use and create our own custom validation attributes, let's see how we can use them to validate our data Model instances from one of their base classes:
ValidationContext validationContext = new ValidationContext(this); List<ValidationResult> validationResults = new List<ValidationResult>(); Validator.TryValidateObject(this, validationContext, validationResults, true);
We start by initializing a ValidationContext object, passing in the data Model instance from the base class. The context object is then passed to the TryValidateObject method of the Validator class, in order to retrieve any validation errors from any of the data annotation attributes.
We also initialize and pass a list of the ValidationResult type to the TryValidateObject method, which will get filled with errors for the current data object. Note that the fourth bool input parameter of this method specifies whether it will return errors for all properties, or just for those that have been decorated with RequiredAttribute from the data annotations namespace.
Later, we'll see how we can incorporate this into our application framework's validation base class, but now let's investigate how we can perform different levels of validation in different scenarios.