Introduction
The namespace System.ComponentModel.DataAnnotations
has a group of classes, attributes and methods, to make validations in our .NET applications.
In the Microsoft world, there are technologies as WPF, Silverlight, ASP MVC, Entity Framework, etc., which make automatic validation with class and exclusive attributes. We think this mechanism is exclusive of this technologies, but it is not like this. We can use it with all classes of Framework.
Index
Add Restriction to Our Classes
The way of adding restrictions to our classes is by attributes in the properties:
public class Customer
{
[Required]
public string Name { get; set; }
public DateTime EntryDate { get; set; }
public string Password { get; set; }
public string PasswordConfirmation { get; set; }
}
In this example, we have used a Required
attribute, but there are more, as we will discussed later.
Validation Attributes Available
All attributes in this section inherits of abstract
class ValidationAttribute.
ValidationAttribute
validate only one Property
in the object.
ValidationAttribute
has an important property, ErrorMessage
. This property gets or sets the custom validation message in case of error.
ErrorMessage
has an implicit ‘FormatString’
, like System.Console.Write
or System.Console.WriteLine
methods, concerning the use of "{0}{1}{2} … {n}"
parameters.
Example:
public class TestClass
{
public string PropertyOne { get; set; }
[MaxLength(2, ErrorMessage = "The property {0} doesn't have more than {1} elements")]
public int[] ArrayInt { get; set; }
}
This is the error validation message:
The sequence of parameters (for ‘StringFormat
’) will be the next:
{0} à PropertyName
{1} à Parameter 1
{2} à Parameter 2
…
{n} à Parameter n
We will study the list of validation attributes available.
CompareAttribute
This attribute, compares the value of two properties. More information can be found here.
public class Customer
{
public string Name { get; set; }
public DateTime EntryDate { get; set; }
public string Password { get; set; }
[Compare("Customer.Password",
ErrorMessage = "The fields Password and PasswordConfirmation should be equals")]
public string PasswordConfirmation { get; set; }
}
This attribute, compare the property marked, with the property linked in its first parameter by a string
argument.
DateTypeAttribute
This attribute allows mark one property/field of way more specific than .NET types. MSDN
says: DateTypeAttribute
specifies the name of an additional type to associate with a data field. Link.
In applications (ASP MVC, Silverlight, etc.) with templates, can be used to changed display data format. For example, one property market with DateTypeAttribute
to Password
, show its data in a TextBox
with “*
” character.
public class Customer
{
public string Name { get; set; }
public DateTime EntryDate { get; set; }
[DataType(DataType.Password)]
public string Password { get; set; }
public string PasswordConfirmation { get; set; }
}
The enum DataType
has the following values:
Custom = 0,
DateTime = 1,
Date = 2,
Time = 3,
Duration = 4,
PhoneNumber = 5,
Currency = 6,
Text = 7,
Html = 8,
MultilineText = 9,
EmailAddress = 10,
Password = 11,
Url = 12,
ImageUrl = 13,
CreditCard = 14,
PostalCode = 15,
Upload = 16,
StringLengthAttribute
Marked the max and the min length of characters allowed in the property/field. Link.
public class Customer
{
[StringLength(maximumLength: 50 , MinimumLength = 10,
ErrorMessage = "The property {0} should have {1} maximum characters
and {2} minimum characters")]
public string Name { get; set; }
public DateTime EntryDate { get; set; }
public string Password { get; set; }
public string PasswordConfirmation { get; set; }
}
MaxLengthAttribute and MinLengthAttribute
These attributes were added to Entity Framework
4.1.
Specify the number maximum and minimum elements in the Array
property. This is valid for string
properties, because one string
is a char[]
too. More information (MaxLengthAttribute, <a>MinLengthAttribute</a>
).
In this example, we can see the two types: for string
and for array
:
public class Customer
{
[MaxLength(50, ErrorMessage = "The {0} can not have more than {1} characters")]
public string Name { get; set; }
public DateTime EntryDate { get; set; }
[DataType(DataType.Password)]
public string Password { get; set; }
[Compare("Customer.Password", ErrorMessage =
"The fields Password and PasswordConfirmation should be equals")]
public string PasswordConfirmation { get; set; }
[MaxLength(2, ErrorMessage = "The property {0} can not have more than {1} elements")]
public int[] EjArrayInt { get; set; }
}
In the case of Array
property, this properties should be Array
, are not valid: List<T>
, Collection<T>
, etc.
RequiredAttribute
Specified that the field is mandatory and it doesn’t contain a null
or string.Empty
values. Link.
public class Customer
{
[Required (ErrorMessage = "{0} is a mandatory field")]
public string Name { get; set; }
public DateTime EntryDate { get; set; }
public string Password { get; set; }
public string PasswordConfirmation { get; set; }
}
RangeAttribute
Specified a range values for a data field. Link.
public class Customer
{
public string Name { get; set; }
[Range(typeof(DateTime), "01/01/1900", "01/01/2014",
ErrorMessage = "Valid dates for the Property {0} between {1} and {2}")]
public DateTime EntryDate { get; set; }
public string Password { get; set; }
[Compare("Customer.Password",
ErrorMessage = "The fields Password and PasswordConfirmation should be equals")]
public string PasswordConfirmation { get; set; }
[Range(0, 150, ErrorMessage = "The Age should be between 0 and 150 years")]
public int Age { get; set; }
}
For the EntryDate
property, the first argument in the RangeAttribute
is the typeof
of Property. For the Age
property isn’t necessary. It is mandatory designate the typeof
data in the first argument
, whenever the property isn’t a numerical type
. If we marked any property not numeric with a RangeAttribute
and not specified this parameter
, the project will not compile.
CustomValidationAttributes
Specified a custom validate method. Link.
For this ValidationAttribute
, we build a new class with a static
method and this signature:
public static ValidationResult MethodValidate( same_type_property_to_validate artument)
Example:
public class CustomerWeekendValidation
{
public static ValidationResult WeekendValidate(DateTime date)
{
return date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday
? new ValidationResult("The wekeend days aren't valid")
: ValidationResult.Success;
}
}
For the property marked, we marked the property with CustomValidate
and add two arguments:
TypeOf
our CustomValidationClass
. - Validator
MethodName
of our CustomValidationClass
. This parameter is a string
type.
public class Customer
{
public string Name { get; set; }
[CustomValidation(typeof(CustomerWeekendValidation),
nameof(CustomerWeekendValidation.WeekendValidate))]
public DateTime EntryDate { get; set; }
public string Password { get; set; }
public string PasswordConfirmation { get; set; }
public int Age { get; set; }
}
Customs Attributes (inherits ValidationAttribute)
This is the last option, it is not available attribute in the DataAnnotations
namespace. Customs Attributes are classes built from scratch, inherits of ValidationAttribute
.
In these classes, we have the freedom to create constructors, properties, etc.
The first step will be write a new class inherits ValidationAttribute
and overwrite IsValid
Method:
Example:
public class ControlDateTimeAttribute : ValidationAttribute
{
private DayOfWeek[] NotValidDays;
private bool ThrowExcepcion;
public ControlDateTimeAttribute(params DayOfWeek[] notValidDays)
{
ThrowExcepcion = false;
NotValidDays = notValidDays;
}
public ControlDateTimeAttribute(bool throwExcepcion, params DayOfWeek[] notValidDays)
{
ThrowExcepcion = throwExcepcion;
NotValidDays = notValidDays;
}
public override bool IsValid(object value)
{
DateTime fecha;
if (!DateTime.TryParse(value.ToString(), out fecha))
{
if (ThrowExcepcion)
throw new ArgumentException(
"The ControlDateTimeAttribute, only validate DateTime Types.");
else
return false;
}
return NotValidDays.Contains(fecha.DayOfWeek);
}
}
This class allows you to select invalid days in the week.
The last step is market DateTime
property with the new attribute:
public class Customer
{
public string Name { get; set; }
[ControlDateTime(DayOfWeek.Monday, DayOfWeek.Tuesday,
DayOfWeek.Wednesday,ErrorMessage = "The {0} isn't valid")]
public DateTime EntryDate { get; set; }
public string Password { get; set; }
public string PasswordConfirmation { get; set; }
public int Age { get; set; }
}
Validation
After this point, we explain all validations forms in DataAnnotations
. We will review Validator
class with all its structure.
DataAnnotations
enables us to work with positive programming, in other words, allows us anticipate exceptions for problems in our business classes or domain classes. It provides better performance
, also exists the negative programming version.
Positive --> TryValidate.
Negative --> Validate.
At this link, there is information about Exceptions
and Performance
.
The examples class.
We will use a new version of Customer
class.
public class Customer
{
[Required(ErrorMessage = "{0} is mandatory")]
[MaxLength(50, ErrorMessage = "The {0} can not have more than {1} characters")]
public string Name { get; set; }
[Range(typeof(DateTime), "01/01/2016", "01/01/2050",
ErrorMessage = "Valid dates for the Property {0} between {1} and {2}")]
public DateTime EntryDate { get; set; }
public string Password { get; set; }
[Compare("Customer.Password",
ErrorMessage = "The fields Password and PasswordConfirmation should be equals")]
public string PasswordConfirmation { get; set; }
[Range(0, 150, ErrorMessage = "The Age should be between 0 and 150 years")]
public int Age { get; set; }
}
These are the principal validations classes:
ValidationResult
ValidationResult
is a container
class validation results. More information can be found here.
ValidationResult
has two properties:
ErrorMessages
- String
readonly
property with the information error description. MemberNames
- IEnumerable<string> readonly
property with
the property name on error.
We have written an extension method to print console the validation errors:
public static string ToDescErrorsString
(this IEnumerable<ValidationResult> source, string mensajeColeccionVacia = null)
{
if (source == null) throw new ArgumentNullException(nameof(source),
$"The property {nameof(source)}, doesn't has null value");
StringBuilder resultado = new StringBuilder();
if (source.Count() > 0)
{
resultado.AppendLine("There are validation errors:");
source.ToList()
.ForEach(
s =>
resultado.AppendFormat(" {0} --> {1}{2}",
s.MemberNames.FirstOrDefault(), s.ErrorMessage,
Environment.NewLine));
}
else
resultado.AppendLine(mensajeColeccionVacia ?? string.Empty);
return resultado.ToString();
}
Validator Class
This is the static
helper class. The Validator
class allows you execute validation on objects, properties
and methods
. More information can be found at this link.
We can separate its methods in two groups, as we saw earlier:
POSITIVE PROGRAMMING (Return bool value and we have a ICollection<ValidationResult> parameter)
The return property bool
is a validation result. The ICollection<ValidationResult>
argument contains details of validations errors. If the validation isn’t errors, this collection will be empty
.
TryValidateObject
public static bool TryValidateObject(object instance, ValidationContext validationContext,
ICollection<ValidationResult> validationResults);
public static bool TryValidateObject(object instance, ValidationContext validationContext,
ICollection<ValidationResult> validationResults, bool validateAllProperties);
This method validates all object. TryValidateObject
has an overload with a bool
argument validateAllProperties
, this argument enables in case of validation error continue with the validation all properties.
Arguments:
Instance
- The instance of class to validate ValidationContext
- The context that describes the object to validate ValidationResults
- The collection of validation results descriptions ValidateAllProperties
- Enabled continue validate all properties
Example:
public static void TryValidateObjectExample1()
{
var customer = new Customer
{
Name = string.Empty,
EntryDate = DateTime.Today,
Password = "AAAA",
PasswordConfirmation = "BBBB",
Age = -1
};
ValidationContext valContext = new ValidationContext(customer, null, null);
var validationsResults = new List<ValidationResult>();
bool correct = Validator.TryValidateObject
(customer, valContext, validationsResults, true);
Console.WriteLine(validationsResults.ToDescErrorsString("Without Errors !!!!"));
Console.Read();
}
In this example, we have done the second overload
with true
value (ValidateAllProperties
).
Result:
We have validated all customer
properties.
If we changed the last argument to false
, it only validates the first property
on error:
public static void TryValidateObjectExample2()
{
var customer = new Customer
{
Name = string.Empty,
EntryDate = DateTime.Today,
Password = "AAAA",
PasswordConfirmation = "BBBB",
Age = -1
};
ValidationContext valContext = new ValidationContext(customer, null, null);
var validationsResults = new List<ValidationResult>();
bool correct =
Validator.TryValidateObject(customer, valContext, validationsResults, false);
Console.WriteLine(validationsResults.ToDescErrorsString("Without Errors !!!!"));
Console.Read();
}
Result:
TryValidateProperty
public static bool TryValidateProperty(object value, ValidationContext validationContext,
ICollection<ValidationResult> validationResults);
It has only one overload. It has the same parameters of TryValidateObject
, but the first object
parameter, does match with one property to validate and the method therefore validates properties.
public static void TryValidatePropertyExample()
{
var customer = new Customer
{
Name = string.Empty,
EntryDate = DateTime.Today,
Password = "AAAA",
PasswordConfirmation = "BBBB",
Age = -1
};
ValidationContext valContext = new ValidationContext(customer, null, null)
{
MemberName = "Age"
};
var validationsResults = new List<ValidationResult>();
bool correct = Validator.TryValidateProperty
(customer.Age, valContext, validationsResults);
Console.WriteLine(validationsResults.ToDescErrorsString("Without Errors !!!!"));
Console.Read();
}
Note
When we instance the ValidationContext
object, we give value to string
property MemberName
, with the property name to validate.
The TryValidateProperty
and the TryValidateObject
, are equals, only change the first parameter. In the TryValidateProperty
will have to be said the property value
.
Result
TryValidateValue
public static bool TryValidateValue(object value, ValidationContext validationContext,
ICollection<ValidationResult> validationResults,
IEnumerable<ValidationAttribute> validationAttributes);
TryValidateValue
validates a value through ValidationAttribute
collection. This is practical to reuse our ValidationAttributes
and us to be disabuses of if
terms.
public static void TryValidateValueExample()
{
string myPwd = "33223";
MinLengthAttribute minLengthAtribute = new MinLengthAttribute(8)
{ ErrorMessage = "{0} must have {1} caracters minimum" };
RequiredAttribute requieredAttribute = new RequiredAttribute
{ ErrorMessage = "{0} is mandatory" };
List<ValidationAttribute> atributes = new List<ValidationAttribute>()
{ minLengthAtribute, requieredAttribute };
ValidationContext valContext = new ValidationContext(myPwd, null, null)
{
MemberName = "myPwd"
};
var validationsResults = new List<ValidationResult>();
bool correct = Validator.TryValidateValue(myPwd, valContext, validationsResults, atributes);
Console.WriteLine(validationsResults.ToDescErrorsString("Without Errors !!!!"));
Console.Read();
}
Note
We have created the attributes in code, because we will validate a value and we don’t have properties to mark in the class declaration.
Result
NEGATIVE PROGRAMMING (throw ValidationException and it hasn’t ICollection<ValidationResult argument )
These methods remove the word Try
in your names. They are void
methods. If the validation fails, it will throw
a ValidationException
. They don’t have a ICollection<ValidationResult>
argument, this information will have inside exception and it isn’t a collection, it is a simple ValidationResult
because if validation fails, it doesn’t verify the remaining properties.
public static void ValidateObject(object instance, ValidationContext validationContext);
public static void ValidateObject
(object instance, ValidationContext validationContext, bool validateAllProperties);
public static void ValidateProperty(object value, ValidationContext validationContext);
public static void ValidateValue(object value, ValidationContext validationContext,
IEnumerable<ValidationAttribute> validationAttributes);
Example:
public static void ValidateObjectExample()
{
var customer = new Customer
{
Name = string.Empty,
EntryDate = DateTime.Today,
Password = "AAAA",
PasswordConfirmation = "BBBB",
Age = -1
};
ValidationContext valContext = new ValidationContext(customer, null, null);
try
{
Validator.ValidateObject(customer, valContext, true);
}
catch (ValidationException ex)
{
ValidationResult s = ex.ValidationResult;
Console.WriteLine("There are validation errors:");
Console.WriteLine(" {0,-20} --> {1}{2}",
s.MemberNames.FirstOrDefault(), s.ErrorMessage,
Environment.NewLine);
}
Console.Read();
}
Result:
The other methods are the same than ‘Try
’ version but with this technique.
Considerations and Tests
We will leave an Extension Method for easier validation tests in all previous examples.
Extension Method:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
namespace DataAnnotations
{
public static class Extensiones
{
public static string ToDescErrorsString
(this IEnumerable<ValidationResult> source, string messageEmptyCollection = null)
{
StringBuilder result = new StringBuilder();
if (source.Count() > 0)
{
result.AppendLine("We found the next validations errors:");
source.ToList()
.ForEach(
s =>
result.AppendFormat(" {0} --> {1}{2}",
s.MemberNames.FirstOrDefault(), s.ErrorMessage,
Environment.NewLine));
}
else
result.AppendLine(messageEmptyCollection ?? string.Empty);
return result.ToString();
}
public static IEnumerable<ValidationResult> ValidateObject(this object source)
{
ValidationContext valContext = new ValidationContext(source, null, null);
var result = new List<ValidationResult>();
Validator.TryValidateObject(source, valContext, result, true);
return result;
}
}
}
Validation Example:
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using DataAnnotationsLib.Extensions;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace DataAnnotationsLib.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TextClass_MaxLenght_ArrayIntProperty_NotValid()
{
TestClass textclass = new TestClass();
textclass.ArrayInt = new int[] { 0, 1, 2 };
var errors = textclass.ValidateObject();
Assert.IsTrue(errors.Any());
Assert.AreEqual(errors.First().ErrorMessage,
$"The property {nameof(textclass.ArrayInt)} doesn't have more than 2 elements");
}
[TestMethod]
public void TextClass_MaxLenght_ArrayIntProperty_Valid()
{
TestClass textclass = new TestClass();
textclass.ArrayInt = new int[] { 0, 1};
var errors = textclass.ValidateObject();
Assert.IsFalse(errors?.Any() ?? false);
}
}
public class TestClass
{
public string PropertyOne { get; set; }
[MaxLength(2, ErrorMessage =
"The property {0} doesn't have more than {1} elements")]
public int[] ArrayInt { get; set; }
}
}
History
- 26th April, 2017: Initial version