Validation is one of the pillars of every application and in this post we're going to discuss the important kind of validation - server-side. Since it's a part of every application I write I spend a lot of time thinking about it so through the years I've honed my approach little by little. Finally I'm starting to feel like I have a grip on all of the possible use cases. Hopefully that sentence is not coming back to haunt me.

So before we start, let me explain what I consider to be properly done validation. It's one that:

1. Doesn't pollute the Controllers

Controllers are not supposed to be doing validation. They're like a postman, carrying the mail from the sender (the user action) to the receiver (the classes doing business logic). The postman doesn't open the mail to check whether it's okay does he? I hope not.

2. Is contained

Just like any other type of business logic validation should be contained. As in, not be scattered throughout the project. One of the more common mistakes developers make is differentiating between view model validation and deep validation (one that hits the database) which causes them to validate the same action in two different places. One usually being the view model for simple checks and the other the service layer for when database checks are necessary. Having validation code scattered like this makes it harder to maintain and reason about.

3. Is readable

One of my pet peeves in .NET is how often people (ab)use DataAnnotations for validation. They're not exactly elegant as they get quite verbose pretty fast and since the annotations are placed just above the properties they're validating the whole view model class becomes littered to the point where it can barely be recognized.

using System.ComponentModel.DataAnnotations;

public class RegisterViewModel
{
    [StringLength(30, ErrorMessageResourceName = "FirstNameExceedsMaxLength", ErrorMessageResourceType = typeof(Resources.DataAnnotations))]
    public string FirstName { get; set; }

    [StringLength(30, ErrorMessageResourceName = "LastNameExceedsMaxLength", ErrorMessageResourceType = typeof(Resources.DataAnnotations))]
    public string LastName { get; set; }

    [Required]
    [EmailAddress(ErrorMessageResourceName = "InvalidEmail", ErrorMessageResourceType = typeof(Resources.DataAnnotations))]
    public string Email { get; set; }

    [Required]
    [MinLength(8, ErrorMessageResourceName = "PasswordIsBelowMinLength", ErrorMessageResourceType = typeof(Resources.DataAnnotations))]
    public string Password { get; set; }

    [Required]
    [Compare("Password", ErrorMessageResourceName = "PasswordsDontMatch", ErrorMessageResourceType = typeof(Resources.DataAnnotations))]
    public string ConfirmPassword { get; set; }
}

Sure, they are easy to write for simple validation checks, but once you need to go to the database things get tricky. You need to create a new custom validation attribute for each custom validation which scatters the logic and often results in way too many validation classes. A well-designed class is one that you can just skim through to understand what it does and because DataAnnotations are missing a convention using them often ends up producing completely disparate view models.

So having said all of that what is my promised solution?

Fluent Validation

For the .NET developers who've been living under a rock for the past seven years: FluentValidation is a .NET library that allows us to write validation rules with the use of lambda expressions and method chaining, in a fluent manner so to speak. We put the validation rules in our view model (or DTO if you're into that) and they're validated on model binding. With that, the validation errors are set in the ModelState object. Having used this library for quite a while I've only recently begun to understand its potential. So let's set it up.

Installation

Since we're using .NET Core we're going to install the FluentValidation.AspNetCore NuGet package. Once that's done the only thing left to do is to hook it up to the request pipeline by calling the AddFluentValidation() method to our Startup.cs file like so:

public void ConfigureServices(IServiceCollection services)
{
    // using FluentValidation.AspNetCore;
    
    services.AddMvc()
            .AddFluentValidation(x => x.RegisterValidatorsFromAssemblyContaining<Startup>());
}

And with that we're ready so let's cover some validation cases.

Simple checks

Common checks for string, integer and enum type properties are a breeze.

using FluentValidation;

public class SimpleValidationViewModel
{
    public string Name { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
    public string ConfirmPassword { get; set; }
    public int Age { get; set; }
    public GenderEnum Gender { get; set; }

    public class SimpleValidationViewModelValidator : AbstractValidator<SimpleValidationViewModel>
    {
        public SimpleValidationViewModelValidator()
        {
            // General
            RuleFor(vm => vm.Name).NotNull().WithMessage("Name is required.");
            RuleFor(vm => vm.Name).NotEmpty().WithMessage("Name cannot be empty.");

            // Strings
            RuleFor(vm => vm.Email).EmailAddress().WithMessage("Email is not a valid email address.");
            RuleFor(vm => vm.Name).MaximumLength(30).WithMessage("Name cannot be longer than 30 characters.");
            RuleFor(vm => vm.Password).MinimumLength(8).WithMessage("Password cannot be shorter than 8 characters.");

            // Compare
            RuleFor(vm => vm.ConfirmPassword).NotEmpty()
                                             .Must((x, confirmPassword) => x.Password.Equals(confirmPassword))
                                             .WithMessage("Passwords must match.");

            // Integers
            RuleFor(vm => vm.Age).GreaterThan(17).WithMessage("Age must be greater than 17.");
            RuleFor(vm => vm.Age).LessThan(60).WithMessage("Age must be less than 60.");
            RuleFor(vm => vm.Age).InclusiveBetween(18, 59).WithMessage("Age must be between 18 and 59 inclusive.");

            // Enums
            RuleFor(vm => vm.Gender).IsInEnum().WithMessage("The selected Gender is not valid.");

            // Chaining
            RuleFor(vm => vm.Name).NotEmpty().WithMessage("Name is required.")
                                  .MinimumLength(2).WithMessage("Name cannot be shorter than 2 characters.")
                                  .MaximumLength(30).WithMessage("Name cannot be longer than 30 characters.");
        }
    }
}

Deep validation

Validation against the database requires a bit more work, but follows the same principles. We inject our services/repositories that do the database stuff in our validator class using .NET Core's built-in dependency injection mechanism. Don't forget to register your services in Startup.cs first.

using FluentValidation;

public class DeepValidationViewModel
{
    public int CountryId { get; set; }
    public int? CityId { get; set; }

    public class DeepValidationViewModelValidator : AbstractValidator<DeepValidationViewModel>
    {
        public DeepValidationViewModelValidator(
            ICountryService countryService,
            ICityService cityService)
        {
            // Always check
            RuleFor(vm => vm.CountryId).Must(countryId => countryService.Exists(countryId)).WithMessage("The selected Country is not available.");

            // Always check async
            RuleFor(vm => vm.CountryId).MustAsync(async (countryId, val) => await countryService.ExistsAsync(countryId)).WithMessage("The selected Country is not available.");

            // Only when a value is present
            When(vm => vm.CityId.HasValue, () => {
                RuleFor(vm => vm.CityId).Must(cityId => cityService.Exists(cityId.Value)).WithMessage("The selected City is not available.");
            });

            // Only when a value is present async
            When(vm => vm.CityId.HasValue, () => {
                RuleFor(vm => vm.CityId).MustAsync(async (cityId, val) => await cityService.ExistsAsync(cityId.Value)).WithMessage("The selected City is not available.");
            });
        }
    }
}

Error message localization

Localizing our error messages would be the same as localizing anything else in .NET Core. We're gonna use the Microsoft.Extensions.Localization NuGet package and IStringLocalizer<T>.

using FluentValidation;
using Microsoft.Extensions.Localization;

public class LocalizedValidationViewModel
{
    public string Email { get; set; }

    public class LocalizedValidationViewModelValidator : AbstractValidator<LocalizedValidationViewModel>
    {
        public LocalizedValidationViewModelValidator(
            IStringLocalizer<LocalizedValidationViewModelValidator> localizer)
        {
            RuleFor(vm => vm.Email).NotEmpty().WithMessage(localizer["EmailIsRequired"])
                                   .EmailAddress().WithMessage(localizer["EmailIsInvalid"]);
        }
    }
}

Now all of the validation for a particular view model is encapsulated within it. Furthermore, it follows a clean convention that doesn't get in the way whilst handling pretty much any type of common or custom validation possible.

What I've shown here is just the tip of what the FluentValidation library can do so even if you encounter a validation case I haven't covered I bet you'll find a functionality in the library that tackles it.