FluentValidation tips C#

karenpayneoregon

Karen Payne

Posted on January 7, 2023

FluentValidation tips C#

FluentValidation tips

FluentValidation is a popular .NET library for building strongly-typed validation rules. You can use this library to replace Data Annotations in your web application or applications that do not natively support Data Annotations. It also provides an easy way to create validation rules for the properties in your models/classes, taking out the complexity of validation.

The provided documentation allows a developer get up to speed quickly although it's easy to miss some of the nuances to stream line validation code.

Let's look at setting validation for the following class.

public class Person
{
    public string UserName { get; set; }
    public string EmailAddress { get; set; }
    public string Password { get; set; }
    public string PasswordConfirmation { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

By the docs the following is all that is needed to setup rules for each property.

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.UserName)
            .NotEmpty()
            .MinimumLength(3);

        RuleFor(person => person.EmailAddress).EmailAddress();
        RuleFor(person => person.Password.Length)
            .GreaterThan(7);

        RuleFor(person => person.Password)
            .Equal(p => p.PasswordConfirmation);

    }
}
Enter fullscreen mode Exit fullscreen mode

To use the PersonValidator, create an instance of the Person class with data that validates correctly.

Person person = new()
{
    UserName = "billyBob",
    Password = "my@Password",
    EmailAddress = "billyBob@gmailcom",
    PasswordConfirmation = "my@Password1"
};
Enter fullscreen mode Exit fullscreen mode

Create an instance of PersonValidator followed by calling the Validate method passing in the person.

PersonValidator validator = new();
ValidationResult result = validator.Validate(person);
Enter fullscreen mode Exit fullscreen mode

Next

if (result.IsValid)
{
    // all properties have valid data
}
else
{
    // one or more properties failed to validate, 
    // iterate through result.Errors to create a message to the user
}
Enter fullscreen mode Exit fullscreen mode

Predicate Validator

Predicate Validator allows a developer to write custom validators. The following sets up a rule for a string property named PhoneNumber to be xxx-xxxx

public static class Extensions
{
    public static IRuleBuilderOptions<T, string> MatchPhoneNumber<T>(this IRuleBuilder<T, string> rule)
        => rule.Matches(@"^(1-)?\d{3}-\d{4}$").WithMessage("Invalid phone number");
}
Enter fullscreen mode Exit fullscreen mode

The validator

public class PhoneNumberValidator : AbstractValidator<Person>
{
    public PhoneNumberValidator()
    {
        RuleFor(person => person.PhoneNumber)
            .MatchPhoneNumber();
    }
}
Enter fullscreen mode Exit fullscreen mode

Then in this case include the validator in the PersonValidator.

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {

        Include(new UserNameValidator());
        Include(new EmailAddressValidator());
        Include(new PasswordValidator());
        Include(new PhoneNumberValidator());

    }
}
Enter fullscreen mode Exit fullscreen mode

Run a test.

[TestMethod]
[TestTraits(Trait.Validation)]
public void InvalidPhoneNumberTest()
{
    // arrange
    var person = EmployeeInstance;

    person.PhoneNumber = "11-999";

    PersonValidator validator = new();

    // act
    ValidationResult result = validator.Validate(person);

    // assert
    Assert.IsTrue(result.HasErrorMessage("Invalid phone number"));

}
Enter fullscreen mode Exit fullscreen mode

EF Core example

In this example the goal is to validate that in a Category table, prior to saving to the database ensure the category name is unique.

public partial class Categories
{
    public int CategoryId { get; set; }
    public string CategoryName { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The following method will be used in the CategoryValidator below.

public static class ValidationHelpers
{
    /// <summary>
    /// Validate we are not going to add a duplicate category name
    /// </summary>
    /// <param name="category"></param>
    /// <param name="name"></param>
    /// <returns></returns>
    public static bool UniqueName(Categories category, string name)
    {
        Context context = new Context();
        var categoryItem = context.Categories.AsEnumerable()
            .SingleOrDefault(cat =>
                string.Equals(cat.CategoryName.ToLower(), name.ToLower(), StringComparison.OrdinalIgnoreCase));



        if (categoryItem == null)
        {
            return true;
        }

        return categoryItem.CategoryId == category.CategoryId;
    }
}
Enter fullscreen mode Exit fullscreen mode

The validator

public class CategoryValidator : AbstractValidator<Categories>
{
    public CategoryValidator()
    {
        RuleFor(category => category.CategoryName)
            .Must((cat, x) => 
                ValidationHelpers.UniqueName(cat, cat.CategoryName));
    }
}
Enter fullscreen mode Exit fullscreen mode

Unit test

  • First test checks to see if there is a category name in the table for Produce which there is.
  • Second test checks to see if there is a category name in the table Coffee where there is not.
[TestClass]
public partial class NorthWindTest : TestBase
{

    [TestMethod]
    [TestTraits(Trait.Validation)]
    public void CategoryNameExistsTest()
    {
        Categories category = new() { CategoryName = "Produce" };

        CategoryValidator validator = new();
        ValidationResult result = validator.Validate(category);

        Assert.IsFalse(result.IsValid);
    }

    [TestMethod]
    [TestTraits(Trait.Validation)]
    public void CategoryNameDoesNExistsTest()
    {
        Categories category = new() { CategoryName = "Coffee" };

        CategoryValidator validator = new();
        ValidationResult result = validator.Validate(category);

        Assert.IsTrue(result.IsValid);
    }
}
Enter fullscreen mode Exit fullscreen mode

ASP.NET

See the FluentValidation documentation which describe several approaches to implement with dependency injection.

Razor Pages

See the following project using EF Core 7 and FluentValidation.AspNetCore.

Tips

Above validation rules are setup in one class

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(person => person.UserName)
            .NotEmpty()
            .MinimumLength(3);

        RuleFor(person => person.EmailAddress).EmailAddress();
        RuleFor(person => person.Password.Length)
            .GreaterThan(7);

        RuleFor(person => person.Password)
            .Equal(p => p.PasswordConfirmation);

    }
}
Enter fullscreen mode Exit fullscreen mode

Using Include method. Simple example, create an Employee class which inherits Person class where the Employee class has the same properties as the Person class along with a property Manager.

public class Employee : Person
{
    public string Manager { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The Include method allows showing rules e.g.

Rule for UserName which can be used for Person and Employee

public class UserNameValidator : AbstractValidator<Person>
{
    public UserNameValidator()
    {
        RuleFor(person => person.UserName)
            .NotEmpty()
            .MinimumLength(3);
    }
}
Enter fullscreen mode Exit fullscreen mode

Create a validator class for EmailAddress which has a builtin Email Address validator but as per the documention only checks to see if the email address has a @ symbol. So a tip in a tip, slip in data annotaions attribute EmailAddressAttribute.

Note
There is not one way to validate an email address that can satisfy all developers so decide what works best for you and implement in the class below.

public class EmailAddressValidator : AbstractValidator<Person>
{
    public EmailAddressValidator()
    {
        RuleFor(person => person.EmailAddress)
            .Must((person, b) => new EmailAddressAttribute().IsValid(person.EmailAddress));
    }
}
Enter fullscreen mode Exit fullscreen mode

Password confirmation

public class PasswordValidator : AbstractValidator<Person>
{

    public PasswordValidator()
    {

        RuleFor(person => person.Password.Length)
            .GreaterThan(7);

        RuleFor(person => person.Password)
            .Equal(p => p.PasswordConfirmation)
            .WithState(x => StatusCodes.PasswordsMisMatch);

    }
}
Enter fullscreen mode Exit fullscreen mode

Setup a Person validator using Include

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {

        Include(new UserNameValidator());
        Include(new EmailAddressValidator());
        Include(new PasswordValidator());
    }
}
Enter fullscreen mode Exit fullscreen mode

Setup a Employee validator using Include

First step is to create a validator class to validate there is a manager for an employee.

Note
Manager names are hard code, consider obtaining manager names from a cached data source.

public class ManagerValidator : AbstractValidator<Employee>
{
    public ManagerValidator()
    {

        List<string> managers = new List<string>() {"Jim Adams", "Mary Jones"};

        RuleFor(emp => emp.Manager)
            .Must((employee, name) => managers.Contains(employee.Manager))
            .WithMessage("Invalid manager name");

    }
}
Enter fullscreen mode Exit fullscreen mode

Next create the Employee validator

public class EmployeeValidator : AbstractValidator<Employee>
{
    public EmployeeValidator()
    {
        Include(new UserNameValidator());
        Include(new PasswordValidator());
        Include(new EmailAddressValidator());
        Include(new ManagerValidator());
        RuleFor(person => person.Manager)
            .NotEmpty();
    }
}
Enter fullscreen mode Exit fullscreen mode

Related

Validating application data with Fluent Validation provides similar code samples along with using a json file to store rule data.

{
  "FirstNameSettings": {
    "MinimumLength": 3,
    "MaximumLength": 10,
    "WithName": "First name"
  },
  "LastNameSettings": {
    "MinimumLength": 5,
    "MaximumLength": 30,
    "WithName": "Last name"
  }
}
Enter fullscreen mode Exit fullscreen mode

And PreValidation

protected override bool PreValidate(ValidationContext<Customer> context, ValidationResult result)
{
    if (context.InstanceToValidate is null)
    {
        result.Errors.Add(new ValidationFailure("", $"Dude, must have a no null instance of {nameof(Customer)}"));
        return false;
    }

    return true;
}
Enter fullscreen mode Exit fullscreen mode

See also

Another repository to check out if there is a need to validate SSN.

Requires

  • Microsoft Visual Studio 2022 17.4.x or higher

Summary

Using FluentValidation is one way to perform validation, may or may not be right for every developer, some may want to use Data Annotations or a third party library like Postsharp.

Source code

See the following GitHub repository

💖 đŸ’Ș 🙅 đŸš©
karenpayneoregon
Karen Payne

Posted on January 7, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related