Click here to Skip to main content
13,866,934 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

12.6K views
33 bookmarked
Posted 26 Apr 2017
Licenced CPOL

DataAnnotations in Depth

Rate this:
Please Sign up or sign in to vote.
The best and more completely 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 ours .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  
  • Validation Attributes Available
  • CompareAttribute
  • DateTypeAttribute
  • StringLenghtAttribute
  • MaxLengthAttribute and MinLengthAttribute
  • RequieredAttribute
  • RangeAttribute
  • CustomValidationAttributes
  • Customs Attributes (inherits ValidationAttribute)
  • Validation
  • ValidationResult
  • Validator class
  • Considerations and tests

Add restriction to our classes  

The way of add 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 get or set 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 info in Link.

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,

 

StringLenghtAttribute

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, MinLengthAttribute).

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.

 

RequieredAttribute

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 whit a RangeAttribute and not specified this parameter, the project not compiler.

 

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:

  1. TypeOf our CustomValidationClass.
  2. 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 build from scratch, inherits of ValidationAttribute.

In this 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 las 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.

In this Link, there is an 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 info Link.

ValidationResult has 2 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 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()
{
    /// 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:

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()
{
    /// 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:

 

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()
{
    /// 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:

 

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()
{
    /// 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:

 

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 fail, will be throw a ValidationException. They don’t have a ICollection<ValidationResult> argument, this information will be have inside exception and it isn’t a collection, it is a simple ValidationResult because if validation fail doesn’t verify 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()
{
    /// 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:

The others methods are the same than ‘Try’ version but with this technique.

 

Considerations and Tests

We will leave an Extension Method for easier validation tests 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; }

    }
}

 

 

License

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

Share

About the Author

Juan Francisco Morales Larios
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

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
Sharp Ninja31-Jul-17 3:51
memberSharp Ninja31-Jul-17 3:51 
QuestionAdd a downloadable code sample, please Pin
BillWoodruff3-May-17 9:44
mveBillWoodruff3-May-17 9:44 
Questioncomplete DataAnnotations list or cheat sheet? Pin
Cheung Tat Ming1-May-17 21:17
memberCheung Tat Ming1-May-17 21:17 
AnswerRe: complete DataAnnotations list or cheat sheet? Pin
Juan Francisco Morales Larios3-May-17 3:16
memberJuan Francisco Morales Larios3-May-17 3: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.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.190214.1 | Last Updated 26 Apr 2017
Article Copyright 2017 by Juan Francisco Morales Larios
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid