5

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.

1
  • I think in general the way validation of this kind is handled is by not having warnings, only errors. Anything that doesn't prevent the user from continuing (i.e. is not an error) is not particularly interesting to the user, and if it is interesting, it's probably not a validation. Example: "This will overdraw your checking account and cause an overdraft fee to be assessed. Do you want to continue anyway?" Nov 7, 2017 at 21:15

2 Answers 2

6

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();
    }  
}
3
  • 1
    +1, this is in general a good approach. However, one should add the overhead of implementing validation is only worth the hassle if there a several properties which can be validated reusing the same kind of rules. This also implies the solution would make more sense when you can make the properties independ from the specific class to which the validated properties belong, currently, validation rules depend hardcoded on "Person", that should be made orthogonal.
    – Doc Brown
    Nov 8, 2017 at 13:27
  • This is the design I was looking for. Use attributes with custom validation
    – Mhd
    Nov 8, 2017 at 13:57
  • @DocBrown - I agree. Nov 8, 2017 at 14:20
1

There are several ways you could go.

The business objects could implement IValidate which you could define as:

public interface IValidate
{
    List<IWarning> GetWarnings();
    List<IError> GetErrors();
}

And thus you add the validation to the classes.

You could create Validators like

public abstract class Validator
{
    public abstract List<IWarning> GetWarnings();
    public abstract List<IError> GetErrors();
}
public class PersonValidator : Validator
{
    private IPerson _Person;
    public PersonValidator(IPerson person)
    {
         _Person = person;
    }
    public override List<IWarning> GetWarnings() { /* logic here */ }
    public override List<IError> GetErrors() { /* logic here */ }
}

When you implement the logic in the validator, you could also read DataAnnotations, or your own attributes for annotation.

I.e. you could create your own annotations which indicate the level:

[AttributeUsage(AttributeTargets.Property)]
public class MyValidationAttribute : Attribute
{
    public Level Level {get;} // error, warning, ...
    public string AllowedCharacters {get;}
    // don't forget the ctor 
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.