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; }
}
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);
}
}
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"
};
Create an instance of PersonValidator
followed by calling the Validate
method passing in the person.
PersonValidator validator = new();
ValidationResult result = validator.Validate(person);
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
}
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");
}
The validator
public class PhoneNumberValidator : AbstractValidator<Person>
{
public PhoneNumberValidator()
{
RuleFor(person => person.PhoneNumber)
.MatchPhoneNumber();
}
}
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());
}
}
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"));
}
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; }
}
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;
}
}
The validator
public class CategoryValidator : AbstractValidator<Categories>
{
public CategoryValidator()
{
RuleFor(category => category.CategoryName)
.Must((cat, x) =>
ValidationHelpers.UniqueName(cat, cat.CategoryName));
}
}
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);
}
}
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);
}
}
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; }
}
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);
}
}
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));
}
}
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);
}
}
Setup a Person validator using Include
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
Include(new UserNameValidator());
Include(new EmailAddressValidator());
Include(new PasswordValidator());
}
}
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");
}
}
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();
}
}
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"
}
}
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;
}
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