Home » ASP » ASP.MVC » Asp.Net MVC Model Validation

Asp.Net MVC Model Validation

I have played around with Data Annotation and IValidatableObject interface. As you may know, these are the two officials way to validate your model. The Data Annotation and the Validate method of the interface IValidatableObject are used when the model binding is executing during the post from the web page to the controller and also when saving with Entity Framework. It is also possible to manually execute the validation manually.

The problem I have encounter was during unit test. If you do the test by yourself you will see that if something is wrong with Data Annotation that the Validate method is not even called. The reason behind this behavior is that Data Annotation are for light validation like if a field is required. If this one is not required, than it does not make sense to go to the Validate method because it will fail further validations. This is quite true but not interesting if you want to have all possibles errors of the object.

Another issue that I had with Data Annotation is that if you do have annotation in a property of your first model class, than they are not validated. A small test that show this behavior is this unit test:

[TestClass]
public class UnitTest1
{
	[TestMethod]
	public void TestMethod2()
	{
		var s = new Annot1();
		var results = s.Validate();
		Assert.AreEqual(2, results.Count());
	}
}

public class Annot1:BaseClass2
{
    public Annot1()
    {
        Annot1Property3 = new Annot2();
    }

    public string Annot1Property1 { get; set; }
    public string Annot1Property2 { get; set; }
    public Annot2 Annot1Property3 { get; set; }
  
}

public class Annot2 : BaseClass2
{
    [Required]
    public string Annot2Property1 { get; set; }
    [Required]
    public string Annot2Property2 { get; set; }
 
}

public abstract class BaseClass2
{
    public IEnumerable<ValidationResult> Validate()
    {
        var results = new List<ValidationResult>();
        var validationContext = new ValidationContext(this, null, null);
        Validator.TryValidateObject(this, validationContext, results, true);
        return results;
    }
}

This test fails even we can should expect to have two with the class Annot2 and its two required field.

How can you have validation from Data Annotation from every inheritance levels and having at the same time every validate method to be executed, even if the field is not there? You need to create your own Validate method that will call the TryValidateObject on every inheritance level. Here is an example that show how to use the base model class that has a validate method. It has the advantage that we can call the validate at each level of inheritance but also just call the base class which will go deeper to validate everything.

public abstract class BaseClass
{
    public IEnumerable<ValidationResult> Validate()
    {
        var results = new List<ValidationResult>();
        var validationContext = new ValidationContext(this, null, null);
        Validator.TryValidateObject(this, validationContext, results, true);
        var r = new List<ValidationResult>(results);

        if (this is IValidatableObject)
        {
            IEnumerable<ValidationResult> errors = (this as IValidatableObject).Validate(new ValidationContext(this));
            r.AddRange(errors);
        }
        var childrenResults = ValidateChildren();
        r.AddRange(childrenResults);
        return r;
    }

    public abstract IEnumerable<ValidationResult> ValidateChildren();

}

public class Class1 : BaseClass, IValidatableObject
{
    public Class1()
    {
        Property1 = new Class2();
    }
    public Class2 Property1 { get; set; }
    [Required]
    public string AString1 { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        return new[] {new ValidationResult("Error from class1")};
    }

    public override IEnumerable<ValidationResult> ValidateChildren()
    {
        var r = new List<ValidationResult>();
        var childrenResult = Property1.Validate();
        r.AddRange(childrenResult);
        return r;
    }
}

public class Class2 :BaseClass, IValidatableObject
{
    public Class2()
    {
        Property2 = new Class3();
    }
    public Class3 Property2 { get; set; }
    [Required]
    public string AString2 { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        yield return new ValidationResult("Error from class2");
    }

    public override IEnumerable<ValidationResult> ValidateChildren()
    {
        var r = new List<ValidationResult>();
        var childrenResult = Property2.Validate();
        r.AddRange(childrenResult);
        return r;
    }
}

public class Class3:BaseClass
{
    [Required]
    public string AString3 { get; set; }

    public override IEnumerable<ValidationResult> ValidateChildren()
    {
        return new List<ValidationResult>();
    }
}
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestMethod1()
    {
        var s = new Class1();
        var results = s.Validate();
        Assert.AreEqual(5, results.Count());
    }
}

This works. You can call the validate directly to Class3 and have 1 validation result. But the main objective is to call the validation on Class1. Class1 has a property that use Class2 and this one has Class3. Three level deeps! The code is not clean because it has some repetition but the problem is solved. Every ValidateChildren call all property that has a class that must be validated. It accumulates all validation results and return it. The validate method explicitly calls the Validate method so even if the Data Annotation has error, this one is executed.

You can find all unit test code in this GitHub Repository.

If you like my article, think to buy my annual book, professionally edited by a proofreader. directly from me or on Amazon. I also wrote a TypeScript book called Holistic TypeScript

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.