There is an excellent library, FluentValidation, that is very handy when it comes to expressing validation logic, and doing it centrally.

Suppose we have the following class:

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

We can express validation logic by inheriting from the generic abstract class AbstractValidator and then supplying our logic.

Like this:

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(x => x.Name)
            .MinimumLength(5)
            .MaximumLength(10);
        
        RuleFor(x => x.Age)
            .GreaterThan(18);
    }
}

Our logic here is:

  1. Age must be greater than 18
  2. Name must have a length between 5 and 10 characters

Our logic to validate is as simple as this:

var person = new Person() { Name = "", Age = 30 };
var validator = new PersonValidator();
validator.ValidateAndThrow(person);

This code will throw an exception if any of the validations is not satisfied.

So the code above will throw this exception:

Validation failed:
 -- Name: The length of 'Name' must be at least 5 characters. You entered 0 characters. Severity: Error

So far so good - if you provide an empty string as a name, it will throw an exception.

Suppose someone supplies a null as the name.

Like so:

var person = new Person() { Name = null, Age = 30 };
var validator = new PersonValidator();
validator.ValidateAndThrow(person);

You’d think the logic for minimum and maximum length would catch this.

You would be wrong.

A null will successfully pass validation!

To address this issue, you need to add an additional validation - NotEmpty()

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .MinimumLength(5)
            .MaximumLength(10);
        
        RuleFor(x => x.Age)
            .GreaterThan(18);
    }
}

This has the additional benefit of rejecting strings that are whitespace.

If you run the validator you should get the following result:

Validation failed:
 -- Name: 'Name' must not be empty. Severity: Error

If you don’t want to throw an exception but want to get the errors anyway, you can do it like this:

var result = validator.Validate(person);

From here you can either get a dictionary of properties and errors, like this:

result.ToDictionary();

Or you can get a single string of all the errors, like this:

result.ToString();

Happy hacking!