Click here to Skip to main content
15,885,366 members
Articles / Programming Languages / C#

DataAnnotations in Depth

Rate me:
Please Sign up or sign in to vote.
4.86/5 (20 votes)
26 Apr 2017CPOL6 min read 33.5K   37   6
The best and more complete way to validate our objects in .NET Framework

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:

C#
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:

C#
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:

Image 1

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.

C#
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.

C#
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:

C#
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.

C#
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:

C#
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.

C#
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.

C#
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.

Image 2

CustomValidationAttributes

Specified a custom validate method. Link.

For this ValidationAttribute, we build a new class with a static method and this signature:

C#
public static ValidationResult MethodValidate( same_type_property_to_validate  artument)

Example:

C#
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:

  1. TypeOf our CustomValidationClass.
  2. Validator MethodName of our CustomValidationClass. This parameter is a string type.
C#
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:

C#
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:

C#
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.

C#
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:

C#
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

C#
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:

C#
public static void TryValidateObjectExample1()
{
    /// 1.- Create a customer
    var customer = new Customer
    {
        Name                 = string.Empty,
        EntryDate            = DateTime.Today,
        Password             = "AAAA",
        PasswordConfirmation = "BBBB",
        Age                  = -1
    };
    /// 2.- Create a context of validation
    ValidationContext valContext = new ValidationContext(customer, null, null);
    /// 3.- Create a container of results
    var validationsResults = new List<ValidationResult>();
    /// 4.- Validate customer
    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:

Image 3

We have validated all customer properties.

If we changed the last argument to false, it only validates the first property on error:

C#
public static void TryValidateObjectExample2()
{
    /// 1 - Create a customer
    var customer = new Customer
    {
        Name                 = string.Empty,
        EntryDate            = DateTime.Today,
        Password             = "AAAA",
        PasswordConfirmation = "BBBB",
        Age                  = -1
    };
    /// 2 - Create a context of validation
    ValidationContext valContext = new ValidationContext(customer, null, null);
    /// 3 - Create a container of results
    var validationsResults = new List<ValidationResult>();
    /// 4 - Validate customer
    bool correct = 
    Validator.TryValidateObject(customer, valContext, validationsResults, false);

    Console.WriteLine(validationsResults.ToDescErrorsString("Without Errors !!!!"));
    Console.Read();
}

Result:

Image 4

TryValidateProperty

C#
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.

C#
public static void TryValidatePropertyExample()
{
    /// 1.- Create a customer
    var customer = new Customer
    {
        Name                 = string.Empty,
        EntryDate            = DateTime.Today,
        Password             = "AAAA",
        PasswordConfirmation = "BBBB",
        Age                  = -1
    };
    /// 2.- Create a context of validation
    ValidationContext valContext = new ValidationContext(customer, null, null)
    {
        MemberName = "Age"
    };
    /// 3.- Create a container of results
    var validationsResults = new List<ValidationResult>();
    /// 4.- Validate customer Age Property
    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

Image 5

TryValidateValue

C#
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.

C#
public static void TryValidateValueExample()
{
    /// 1 - Create value
    string myPwd = "33223";
    /// 2 - Create ValidationsAttributes
    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 };
    /// 3 - Create a context of validation
    ValidationContext valContext = new ValidationContext(myPwd, null, null)
    {
        MemberName = "myPwd"
    };
    /// 4 - Create a container of results
    var validationsResults = new List<ValidationResult>();
    /// 5 - Validate myPwd value
    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

Image 6

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.

C#
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:

C#
public static void ValidateObjectExample()
{
    /// 1 - Create a customer
    var customer = new Customer
    {
        Name                 = string.Empty,
        EntryDate            = DateTime.Today,
        Password             = "AAAA",
        PasswordConfirmation = "BBBB",
        Age                  = -1
    };
    /// 2 - Create a context of validation
    ValidationContext valContext = new ValidationContext(customer, null, null);

    try
    {
        /// 3 - Validate customer
        Validator.ValidateObject(customer, valContext, true);
    }
    catch (ValidationException ex)
    {
        /// 4 - Print Validations Results
        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:

Image 7

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:

C#
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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Cecabank
Spain Spain
MVP C# Corner 2017

MAP Microsoft Active Professional 2014

MCPD - Designing and Developing Windows Applications .NET Framework 4
MCTS - Windows Applications Development .NET Framework 4
MCTS - Accessing Data Development .NET Framework 4
MCTS - WCF Development .NET Framework 4

Comments and Discussions

 
QuestionvalidationAttributes explanation is not correct Pin
rocksoccer25-Jun-21 7:16
rocksoccer25-Jun-21 7:16 
QuestionMessage Closed Pin
28-May-21 6:42
Member 1522086028-May-21 6:42 
QuestionDataType validations Pin
Member 1316738427-May-20 11:23
Member 1316738427-May-20 11:23 
GeneralMy vote of 5 Pin
Sharp Ninja31-Jul-17 2:51
Sharp Ninja31-Jul-17 2:51 
Thank you! This demystifies many aspects of validation. Will be used heavily in the future!
QuestionAdd a downloadable code sample, please Pin
BillWoodruff3-May-17 8:44
professionalBillWoodruff3-May-17 8:44 
Questioncomplete DataAnnotations list or cheat sheet? Pin
Cheung Tat Ming1-May-17 20:17
Cheung Tat Ming1-May-17 20:17 
AnswerRe: complete DataAnnotations list or cheat sheet? Pin
Juan Francisco Morales Larios3-May-17 2:16
Juan Francisco Morales Larios3-May-17 2:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.