2. Using Validation Attributes
Having seen how you can use rule sets defined in configuration,
and how you can display the results of a validation process, we can
move on to explore the other ways you can define validation rules in
your applications. The example application contains two classes that
contain validation attributes and a self-validation method. The
AttributedProduct class contains
Validation block attributes, while the AnnotatedProduct class contains data annotation
attributes.
Using the Validation Block Attributes
The example, Using Validation Attributes and
Self-Validation, demonstrates use of the Validation block
attributes. The AttributedProduct
class has a range of different Validation block attributes applied
to the properties of the class, applying the same rules as the
MyRuleset rule set defined in
configuration and used in the previous examples.
For example, the ID property
carries attributes that add a Not Null validator, a String Length
validator, and a Regular Expression validator. These validation
rules are, by default, combined with an And operation, so all of the conditions must
be satisfied if validation will succeed for the value of this
property.
[NotNullValidator(MessageTemplate = "You must specify a product ID.")] [StringLengthValidator(6, RangeBoundaryType.Inclusive, 6, RangeBoundaryType.Inclusive, MessageTemplate = "Product ID must be {3} characters.")] [RegexValidator("[A-Z]{2}[0-9]{4}", MessageTemplate = "Product ID must be 2 letters and 4 numbers.")] public string ID { get; set; }
Other validation attributes used within the AttributedProduct class include an Enum
Conversion validator that ensures that the value of the ProductType property is a member of the
ProductType enumeration, shown here.
Note that the token {3} for the
String Length validator used in the previous section of code is the
lower bound value, while the token {3} for the Enum Conversion validator is the
name of the enumeration it is comparing the validated value
against.
[EnumConversionValidator(typeof(ProductType), MessageTemplate = "Product type must be a value from the '{3}' enumeration.")] public string ProductType { get; set; }
Combining Validation Attribute Operations
One other use of validation attributes worth a mention here is the
application of a composite validator. By default, multiple
validators defined for a member are combined using the And operation. If you want to combine
multiple validation attributes using an Or operation, you must apply the ValidatorComposition attribute first and
specify CompositionType.Or. The
results of all validation operations defined in subsequent
validation attributes are combined using the operation you specify
for composition type.
The example class uses a ValidatorComposition attribute on the
nullable DateDue property to
combine a Not Null validator and a Relative DateTime validator.
The top-level error message that the user will see for this
property (when you do not recursively iterate through the contents
of the ValidationResults) is the
message from the ValidatorComposition attribute.
[ValidatorComposition(CompositionType.Or, MessageTemplate = "Date due must be between today and six months time.")] [NotNullValidator(Negated = true, MessageTemplate = "Value can be NULL or a date.")] [RelativeDateTimeValidator(0, DateTimeUnit.Day, 6, DateTimeUnit.Month, MessageTemplate = "Value can be NULL or a date.")] public DateTime? DateDue { get; set; }
If you want to allow null values for a member of a class,
you can apply the IgnoreNulls
attribute.
Some validation rules are too complex to apply using the
validators provided with the Validation block or the .NET Data Annotation
validation attributes. It may be that the values you need to
perform validation come from different places, such as properties,
fields, and internal variables, or involve complex
calculations.
In this case, you can define self-validation rules as
methods within your class (the method names are irrelevant). We've implemented a self-validation
routine in the AttributedProduct
class in the example application. The method simply checks that
the combination of the values of the InStock, OnOrder, and DateDueAttributedProduct class to see the
implementation. properties meets predefined rules.
You can examine the code within the
Results of the Validation Operation
The example creates an invalid instance of the AttributedProduct class shown above,
validates it, and then displays the results of the validation process. It creates the following output,
though we have removed some of the repeated output here for
clarity. You can run the example yourself to see the full
results.
Created and populated a valid instance of the AttributedProduct class. There were no validation errors. Created and populated an invalid instance of the AttributedProduct class. The following 7 validation errors were detected: + Target object: AttributedProduct, Member: ID - Detected by: RegexValidator - Validated value was: '12075' - Message: 'Product ID must be 2 capital letters and 4 numbers.' ... ... + Target object: AttributedProduct, Member: ProductType - Detected by: EnumConversionValidator - Validated value was: 'FurryThings' - Message: 'Product type must be a value from the 'ProductType' enumeration.' ... ... + Target object: AttributedProduct, Member: DateDue - Detected by: OrCompositeValidator - Validated value was: '19/08/2010 15:55:16' - Message: 'Date due must be between today and six months time.' + Nested validators: - Detected by: RelativeDateTimeValidator - Validated value was: '18/11/2010 13:36:02' - Message: 'Value can be NULL or a date.' - Detected by: NotNullValidator - Validated value was: '18/11/2010 13:36:02' + Target object: AttributedProduct, Member: ProductSelfValidation - Detected by: [none] - Tag value: - Message: 'Total inventory (in stock and on order) cannot exceed 100 items.'
Notice that the output includes the name of the type and the
name of the member (property) that was validated, as well as
displaying type of validator that detected the error, the current
value of the member, and the message. For the DateDue property, the output shows the two
validators nested within the Or Composite validator. Finally, it
shows the result from the self-validation method. The values you
see for the self-validation are those the code in the
self-validation method specifically added to the Validation Results instance.
Validating Subclass Types
While discussing validation through attributes, we should briefly
touch on the factors involved when you validate a class that
inherits from the type you specified when creating the validator
you use to validate it. For example, if you have a class named
SaleProduct that derives from
Product, you can use a validator
defined for the Product class to
validate instances of the SaleProduct class. The Validate method will also apply any
relevant rules defined in attributes in both the SaleProductProduct base class. class and the
If the derived class inherits a member from the base class
and does not override it, the validators for that member defined
in the base class apply to the derived class. If the derived class
inherits a member but overrides it, the validators defined in the
base class for that member do not apply to the derived
class.
Validating Properties that are Objects
In many cases, you may have a property of your class defined
as the type of another class. For example, your OrderLine class is likely to have a
property that is a reference to an instance of the Product class. It's common for this
property to be defined as a base type or interface type, allowing
you to set it to an instance of any class that inherits or
implements the type specified for the property.
You can validate such a property using an ObjectValidator attribute within the class.
However, by default, the validator will validate the property
using rules defined for the type of the property—in this example
the type IProduct. If you want the
validation to take place based on the actual type of the object
that is currently set as the value of the property, you can add
the ValidateActualType parameter to
the ObjectValidator attribute, as
shown here.
public class OrderLine { [ObjectValidator(ValidateActualType=true)] public IProduct OrderProduct { get; set; } ... }
Using Data Annotation Attributes
The System.ComponentModel.DataAnnotations namespace in the
.NET Framework contains a series of attributes that you can add to
your classes and class members to signify metadata for these classes
and members. They include a range of validation attributes that you can use to apply
validation rules to your classes in much the same way as you can
with the Validation block attributes. For example, the
following shows how you can use the Range attribute to specify that
the value of the property named OnOrder must be between 0 and 50.
[Range(0, 50, ErrorMessage = "Quantity on order must be between 0 and 50.")] public int OnOrder { get; set; }
Compared to the validation attributes provided with the Validation
block, there are some limitations when using the validation
attributes from the DataAnnotations namespace:
-
The range of supported validation operations is less
comprehensive, though there are some new validation types
available in .NET Framework 4.0 that extend the range. However,
some validation operations such as property value comparison,
enumeration membership checking, and relative date and time
comparison are not available when using data annotation
validation attributes. -
There is no capability to use Or composition, as there is with the Or
Composite validator in the Validation block. The only
composition available with data annotation validation attributes
is the And operation. -
You cannot specify rule sets names, and so all rules
implemented with data annotation validation attributes belong to
the default rule set. -
There is no simple built-in support for self-validation, as
there is in the Validation block.
You can, of course, include both data annotation and
Validation block attributes in the same class if you wish, and
implement self-validation using the Validation block mechanism in a
class that contains data annotation validation attributes. The
validation methods in the Validation block will process both types
of attributes.
An Example of Using Data Annotations
The class named AnnotatedProduct
contains data annotation attributes to implement
the same rules as those applied by Validation block attributes in
the Attributed Product class (which
you saw in the previous example). However, due to the limitations
with data annotations, the self-validation method within the class
has to do more work to achieve the same validation rules.
For example, it has to check the minimum value of some
properties as the data annotation attributes in version 3.5 of the
.NET Framework only support validation of the maximum value (in
version 4.0, they do support minimum value validation). It also
has to check the value of the DateDue property to ensure it is not more
than six months in the future, and that the value of the ProductType property is a member of the
ProductType enumeration.
To perform the enumeration check, the self-validation method
creates an instance of the Validation block Enum Conversion
validator programmatically, and then calls its DoValidate method (which allows you to pass
in all of the values required to perform the validation). The code
passes to this method the value of the ProductType property, a reference to the
current object, the name of the enumeration, and a reference to
the ValidationResults
instance being use to hold all of the validation
errors.
var enumConverterValidator = new EnumConversionValidator(typeof(ProductType), "Product type must be a value from the '{3}' enumeration."); enumConverterValidator.DoValidate(ProductType, this, "ProductType", results);
The code that creates the object to validate, validates it,
and then displays the results is the same as you saw in the
previous example, with the exception that it creates an invalid
instance of the AnnotatedProduct
class, rather than the AttributedProduct
class. The result when you run this example is also
similar to that of the previous example, but with a few
exceptions. We've listed some of the output here.
Created and populated an invalid instance of the AnnotatedProduct class. The following 7 validation errors were detected: + Target object: AnnotatedProduct, Member: ID - Detected by: [none] - Tag value: - Message: 'Product ID must be 6 characters.' ... + Target object: AnnotatedProduct, Member: ProductSelfValidation - Detected by: [none] - Tag value: - Message: 'Total inventory (in stock and on order) cannot exceed 100 items.' + Target object: AnnotatedProduct, Member: ID - Detected by: ValidationAttributeValidator - Message: 'Product ID must be 2 capital letters and 4 numbers.' + Target object: AnnotatedProduct, Member: InStock - Detected by: ValidationAttributeValidator - Message: 'Quantity in stock cannot be less than 0.'
You can see that validation failures detected for data
annotations contain less information than those detected for the
Validation block attributes, and validation errors are shown as
being detected by the ValidationAttributeValidator class—the base
class for data annotation validation attributes. However, where we
performed additional validation using the self-validation method, there is extra
information available.
Defining Attributes in Metadata Classes
In some cases, you may want to locate your validation
attributes (both Validation block attributes and .NET Data
Annotation validation attributes) in a file separate from the one
that defines the class that you will validate. This is a common
scenario when you are using tools that generate the class files, and
would therefore overwrite your validation attributes. To avoid this
you can locate your validation attributes in a separate file that
forms a partial class along with the main class file. This approach
makes use of the Meta dataType
attribute from the System.ComponentModel.DataAnnotations
namespace.
You apply the MetadataType
attribute to your main class file, specifying the
type of the class that stores the validation attributes you want to apply to your main class
members. You must define this as a partial class, as shown here.
[MetadataType(typeof(ProductMetadata))] public partial class Product {
... Existing members defined here, but without attributes or annotations ... }
You then define the metadata type as a normal class, except
that you declare simple properties for each of the members to which you
want to apply validation attributes. The actual type of these
properties is not important, and is ignored by the compiler. The
accepted approach is to declare them all as type Object. As an example, if your Product class contains the ID and Description properties, you can define the
metadata class for it, as shown here.
public class ProductMetadata { [Required(ErrorMessage = "ID is required.")] [RegularExpression("[A-Z]{2}[0-9]{4}", ErrorMessage = "Product ID must be 2 capital letters and 4 numbers.")] public object ID; [StringLength(100, ErrorMessage = "Description must be less than 100 chars.")] public object Description; }
Specifying the Location of Validation Rules
When you use a validator obtained from the ValidatorFactory, as we've done so far in the
example, validation will take into account any applicable rule sets
defined in configuration and in attributes and self-validation
methods found within the target object. However, you can resolve
different factory types if you want to perform validation using only
rule sets defined in configuration, or using only attributes and
self-validation. The specialized types of factory you can use
are:
-
ConfigurationValidatorFactory
. This factory creates validators that only apply
rules defined in a configuration file, or in a configuration
source you provide. By default it looks for configuration in the
default configuration file (App. config or Web.config). However,
you can create an instance of a class that implements the
IConfigurationSource interface,
populate it with configuration data from another file or
configuration storage media, and use this when you create this
validator factory. -
AttributeValidatorFactory
. This factory creates validators that only apply
rules defined in Validation block attributes located in the target
class, and rules defined through self-validation methods. -
ValidationAttributeValidatorFactory
. This factory creates validators that only apply
rules defined in .NET Data Annotations validation
attributes.
For example, to obtain a validator for the Product class that validates using only
attributes and self-validation methods within the target instance,
and validate an instance of this class, you resolve an instance of
the AttributeValidatorFactory from
the container, as shown here.
AttributeValidatorFactory attrFactory = EnterpriseLibraryContainer.Current.GetInstance<AttributeValidatorFactory>(); Validator<Product> pValidator = attrFactory.CreateValidator<Product>(); ValidationResults valResults = pValidator.Validate(myProduct);
|