shyaway

DataAnnotation > DataAnnotation Validation and IValidatableObject 본문

.NET

DataAnnotation > DataAnnotation Validation and IValidatableObject

shyaway 2017. 12. 18. 08:55

DataAnnotation Validation and IValidatableObject


DataAnnotation?

Means annotations that validate member variables in a class like [Required]. The main purpose is to validate data types that are explicit and have a clear format. So .NET already has a series of classes for certain data validation, see the list below.


    • CreditCardAttribute
    • EmailAddressAttribute
    • MaxLengthAttribute
    • MinLengthAttribute
    • StringLengthAttribute
    • UrlAttribute
    • RequiredAttribute
    • PhoneAttribute

You can see all list of classes in System.ComponentModel.DataAnnotations here.  


It basically inherits the base class ValidationAttribute and the base class implements Attribute interface.




All the attributes that developers can put up on a member or class is derived from Attribute interface, including [DllImport] for Pinvoke.



IValidatableObject?

It provides a way to validate a complex object validation that a single data annotation attribute cannot just deal with. Annotation Attribute can not be used when verifying validity of data by applying conditions to multiple data because validation of property unit is the primary purpose. So that's what the IValidatableObject interface for.



How it works?

Let's see an actual example. in MVC app, it automatically processes validations on properties on a model by detecting the attributes on them and put the results in ModelState, the key/value pair object, in ViewBag object, and eventually returns it with http response. You can access the key value object in Razor directly so once you bind the message key on the cshtml, MVC now can show the result automatically in the front. This time, I'm not going to deal with MVC example. I'm going to show how it works in an application like console, winform, wpf app.


Sample Model

1
2
3
4
5
6
7
8
9
10
11
12
public Company
{
    [Required]
    public string CompanyName { get; set; }
 
    [Range(1, int.MaxValue)]
    public int Employees { get; set; }
 
    public List<Employee> EmployeeList { get; set; }
 
    public List<Department> DepartmentList { get; set; }
}

Perform validation in application code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void SomeValidationLogic()
{
    Company company = new Company();           
    List<ValidationResult> results = new List<ValidationResult>();
    ValidationContext context = new ValidationContext(company, null, null);          
    if(Validator.TryValidateObject(company, context, results, true))
    {    
        // Validation passed !
    }
    else
    {    
        // Validation failed !     
    }
}

As you can see above, you will need three prerequisites to manually perform validation on a model.


    • ValidationResult
      It has ErrorMessage and MemberNames as its member variable. The error message and the property name will be bound automatically while the validation process. And you just toss the list of ValidationResult into TryValidateObject method, and it internally adds the result.

    • ValidationContext
      It's literally a context of validation process. It provides IServiceProvider for you to perform DI ( Dependency Injection ) later, but I'm not going to skip it.

    • Validator
      It's a static helper class. All you have to remember is TryValidatObject.

You just declare these classes and pass them into TryValidateObject method, the it will do all the heavy lefting for you. What you're gonna do is just check the ValidationResult list. As you expected, the code above will throws 2 errors.

    1. [Required] validation result >> CompanyName is required.
    2. [Range] validation result >> Employees value should be in range between 1 and 2,147,483,647.

CompanyName is supposed to be Null there, and if you don't declare nothing on Employees, it automatically sign 0 value, so you will have those errors. If you want to get the message and property name respectably, see below code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void SomeValidationLogic()
{
    Company company = new Company();
    List<ValidationResult> results = new List<ValidationResult>();     
    ValidationContext context = new ValidationContext(company, null, null);
    if(Validator.TryValidateObject(company, context, results, true))
    {
        // Validation passed !
    }
    else
    {
        foreach(var e in results)
        {
            string ErrorMessage = e.ErrorMessage;    
            // Suitable if there is only one property name. It could have multiple names.
            string PropertyName = e.MemberNames.FirstOrDefault();    
        }
    }
}
 

If you want more complex validation... use IValidatableObject

As I mentioned before, validating property unit is not enough. If you need to scrutinize the nested Employee and Department model list in Company model, for example, you are going to use if else in the SomeValidationLogic context. But it'd be better if you can isolate the validation area for composing more logical process and it will improve readability of your codes.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Company : IValidatableObject
{
    [Required]
    public string CompanyName { get; set; }
 
    [Range(1, int.MaxValue)]
    public int Employees { get; set; }
     
    public List<Employee> EmployeeList { get; set; }
 
    public List<Department> DepartmentList { get; set; }
 
    public IEnumerable<ValidationResult> Validate(object value)
    {
        Company company = value as company;
        if(company != null) {
        if(company.EmployeeList.Count <= 0 && company.DepartmentList.Count <= 0)
        {
          //               ValidationResult(" Error Message ", " MemberNames " )
          yield return new ValidationResult("No Department and Employees information", new string[] { "DepartmentList & EmployeeList" });
        }
        if(company.EmployeeList.Count > 10)
        {
          yield return new ValidationResult("This company doesn't have more than 10 departments", new string[] { "DepartmentList" });
 
            }        
      }        
}

You can tailor the validation job by implementing IValidatableObject. You just use yield return with ValidationResult instance, providing your own error messages and member names. You can ignore the member name by the way.


Caution on IValidatableObject

TryValidateObject has a hidden, default behaviour. It has a certain priority when it tries to validate an ojbect. So it means, if there's an error in higher priority checking sequence, the lower priority will be completely ignored. see the priority below.

    1. [Required]
    2. Other attributes
    3. IValidatableObject Implementation

So [Required] takes the top priority. Other validations will be executed eventually only if [Required] validation has been passed. And IValidatableObject takes the lowest priority. It's going to be called only if there's no [Required] error and no other errors like [Range]. It provides an option to guarantee to perform validation to the second priority. Let's see below boolean parameter.
1
2
3
4
5
6
7
8
if(Validator.TryValidateObject(company, context, results, true)) // << That last parameter.
{
    // Validation passed !
}
else
{
    // Validation failed !
}


It means validateAllProperties. It is literally a decision to check all the properties or not. If you set true, it will validate all the annotations in the Company model, including [Required] and [Range]. If you set false, it will validate [Required] property only. All the other annotations will be completely ignored. It's quite alright when validating [Required] will suffice for you, but there's a certain circumstances that you need a total set of validation results.


Seeing the limitations on data annotations and IValidatableObject implementation, it's impossible to make sure to validate all properties and invoke IValidatableObject implementation in a single validation context. How can I achieve that? I'm going to write another post about how to make a custom attribute and a way to guarantee to call the interface implemented method even if there's an prior error in the validation context.
























Comments