How to Implement Multi-Level Validation in C#

cclicsvvalidation

I have a Console project reads inputs from CSV file and tries to save them to database.

For that, I created a class Person that maps a CSV row.

The CSV file has two columns Name and Age. Person class is like.

class Person
{
    public string Name;
    public int Age;
}

So the list of all populated objects is List<Person>.

I have a new requirement to display validation messages to console before proceed with saving populated objects to database.

Validation has two levels: Error and Warning.

For example if Name property contains a special character, I have to display this message: "Error: Name contains special character"

In case Name properly contains a numeric character, I have to display only warning message: "Warning: Name contains numeric character"

I was thinking about using DataAnnotation but I cannot see a way to add different levels (Error and Warning) to validation process. Also, I'm not sure if DataAnnotation fits only in Web Applications.

Is there a way to add some functionality to Person class to get this validation done for each property?

NB: This is just an example to better understand the question, I have other rules for other properties.

Best Answer

Here is my solution. It is not perfect, but it is a start.

First of all, I have created an enum called ErrorLevel:

enum ErrorLevel
{
    Error,
    Warning
}

Next, for every validation rule, I have created a Custom Attribute. Each Custom Attributes has a private field ErrorLevel, and a Validate method which returns the error description or null if no error was found (I created an IValidationRule<T> interface to ensure that this method exists).

IValidationRule<T>:

interface IValidationRule<T>
{
    string Validate(T input);
}

NoNumbersAttribute:

class NoNumbersAttribute : Attribute, IValidationRule<Person>
{
    private ErrorLevel errorLevel;

    public NoNumbersAttribute(ErrorLevel errorLevel)
    {
        this.errorLevel = errorLevel;
    }

    public string Validate(Person person)
    {
        if(person.Name.Any(c => char.IsDigit(c)))
        {
            return $"{errorLevel.ToString()}: 'Name' contains numeric characters. ";
        }

        return null;
    }
}

NoSpecialCharactersAttribute:

class NoSpecialCharactersAttribute : Attribute, IValidationRule<Person>
{
    private ErrorLevel errorLevel;

    public NoSpecialCharactersAttribute(ErrorLevel errorLevel)
    {
        this.errorLevel = errorLevel;
    }

    public string Validate(Person person)
    {
        if (!new Regex("^[a-zA-Z0-9 ]*$").IsMatch(person.Name))
        {
            return $"{errorLevel.ToString()}: 'Name' contains special character.";
        }

        return null;
    }
}

Notice that we use the errorLevel to ensure the output will contain the correct error level.

Now, in the Person class I changed the fields to properties since you really do not want public fields. I have added the Custom Attributes on top of the relevant properties:

Person:

class Person
{
    [NoSpecialCharacters(ErrorLevel.Error)]
    [NoNumbers(ErrorLevel.Warning)]
    public string Name { get; }

    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

And finally, I have created a method that gets a list of persons, gets their validation results and print them:

ValidatePeople:

static bool ValidatePeople(List<Person> people)
{
    bool isValid = true;

    foreach (Person person in people)
    {
        PropertyInfo[] properties = typeof(Person).GetProperties();
        foreach (PropertyInfo property in properties)
        {
            var rules = Array.ConvertAll(property.GetCustomAttributes(typeof(IValidationRule<Person>), true), item => (IValidationRule<Person>)item);
            var failures = rules.Select(r => r.Validate(person)).Where(f => f != null);

            failures.ToList().ForEach(f => Console.WriteLine($"({person.Name}) {f}"));
            if (failures.Count() > 0) isValid = false;
        }
    }

    return isValid;
}

So the Main method will look something like this:

static void Main(string[] args)
{
    List<Person> people = GetPeopleFromCSV();
    if(ValidatePeople(people))
    {
        InsertPeopleToDatabase();
    }  
}