FluentValidation is an open-source library for .NET that provides a fluent API for building validation rules for objects. It allows developers to express the rules for validating an object in a clear and readable way, making it easy to understand and maintain.
In this article, we will explore FluentValidation and how it can be used to specify some rule sets and then state the sets of rules to be executed or not in a .NET application. Whether you are new to FluentValidation or an experienced user, this article will provide valuable insights and examples to help you get the most out of this powerful library.
Initial setup
- Create a new console application in Visual Studio by going to File > New > Project and selecting “Console App (.NET 6)”.
- Install the FluentValidation and Autofac NuGet packages by running the following command in the package manager console:
Install-Package FluentValidation
Install-Package AutofacModels
In this section, we will define the Customer model used throughout the article to apply the capabilities of FluentValidation to it.
The Customer model:
namespace FluentValidationExample.Models;
internal class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string Status { get; set; }
public Address Address { get; set; }
}The Address model:
namespace FluentValidationExample.Models;
internal class Address
{
public string Street { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
}Validations
A “RuleSet” is a way to group multiple validation rules together in FluentValidation. The “RuleSet” method can be used to specify the name of the RuleSet and a lambda function that contains the validation rules for that RuleSet.
Let’s use some constants to easily identify and maintain the different validation rules in the application.
namespace FluentValidationExample.Validators;
internal class ValidationConstants
{
public const string BasicValidation = nameof(BasicValidation);
public const string AddressValidation = nameof(AddressValidation);
}Let’s now create a Customer Validator:
using FluentValidation;
using FluentValidationExample.Models;
namespace FluentValidationExample.Validators;
internal class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator(IValidator<Address> addressValidator)
{
RuleSet(ValidationConstants.BasicValidation, () =>
{
RuleFor(customer => customer.Name).NotNull();
RuleFor(customer => customer.Surname).NotNull();
});
RuleFor(customer => customer.Status).NotNull();
RuleSet(ValidationConstants.AddressValidation, () =>
{
RuleFor(customer => customer.Address)
.NotEmpty()
.DependentRules(() => RuleFor(x => x.Address).SetValidator(addressValidator));
});
}
}The validator is a class called “CustomerValidator” that inherits from the “AbstractValidator” class, and is passed a generic type of “Customer.”
The constructor for the validator takes in an IValidator<Address> object, which is used to validate the customer’s address.
In the constructor, the first block sets up the first “RuleSet” for the validator, which is named “BasicValidation.” The first rule in the set is that the “Name” property of the customer object is not null. The second rule is that the “Surname” property of the customer object is not null. These rules are grouped together under the “BasicValidation” RuleSet.
Then there is a rule for setting up a rule that the “Status” property of the customer object is not null.
The last block sets up the second “RuleSet” for the validator, which is named “AddressValidation.” The first rule in the set is that the “Address” property of the customer object is not empty. The second rule is dependent on the address validator and the address property of the customer object. These rules are grouped together under the “AddressValidation” RuleSet.
Let’s define the Address validator:
using FluentValidation;
using FluentValidationExample.Models;
namespace FluentValidationExample.Validators;
internal class AddressValidator : AbstractValidator<Address>
{
public AddressValidator()
{
RuleFor(address => address.Street).NotNull();
RuleFor(address => address.City).NotNull();
RuleFor(address => address.PostalCode).NotNull();
}
}The class has a constructor that takes no parameters and inside the constructor, it has three validation rules defined using RuleFor method that checks if the street, city, and postal code properties of the address object are not null.
Manage dependencies with Autofac
Autofac is an open-source Inversion of Control (IoC) container for .NET. It is used to manage the dependencies between classes in a software application. The main purpose of an IoC container is to manage the creation and lifetime of objects, and it allows developers to focus on writing the application logic rather than worrying about the details of object creation and management.
An IoC container does this by providing a way to register types and their dependencies and then resolve them when they are needed. It also provides services like lifetime management, so that objects can be reused or disposed of properly.
In the example that follows, Autofac is used to automatically register and resolve validators in a FluentValidation-based application. By using Autofac, the developer does not need to manually register each validator, instead, it scans assemblies for validator classes and registers them with the container, which makes it easy to manage and update the validators in the application.
using Autofac;
using FluentValidationExample.Autofac;
namespace FluentValidationExample;
public static class AutofacConfig
{
public static IContainer Configure()
{
var builder = new ContainerBuilder();
builder.RegisterValidators();
return builder.Build();
}
}This code defines a AutofacConfig class, which has a single public method named Configure. This method creates a new instance of ContainerBuilder, which is used to configure and build the Autofac container. The RegisterValidators extension method is called on the builder object to register validators with the container. The Build method is called on the builder object to create an instance of the container, and it returns the container.
using Autofac;
using FluentValidation;
namespace FluentValidationExample.Autofac;
public static class AutofacExtension
{
public static void RegisterValidators(this ContainerBuilder builder)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
builder.RegisterAssemblyTypes(assemblies)
.Where(t => t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValidator<>)))
.AsImplementedInterfaces();
}
}This code defines an AutofacExtension class, which has a single public method named RegisterValidators. This method is an extension method for ContainerBuilder, which allows it to be called on an instance of ContainerBuilder as if it were a method on the class. The method takes the current AppDomain assemblies and register assembly types that implement IValidator<> interface and are named as the implemented interface.
Validation executions
FluentValidation allows you to trigger validation by creating a validation context and providing a validator selector to filter which validation rules should be executed. The validator selector is used to determine which validation rules should be executed. FluentValidation provides several built-in validator selectors, such as RulesetValidatorSelector and MemberNameValidatorSelector. You can also create your own custom validator selector by implementing the IValidatorSelector interface.
By default, when you call the Validate method on the validator, it will execute all the rulesets defined in the validator. You can also specify a specific set of rulesets to be included or excluded by passing an array of strings representing the names of the rulesets to the validator selector. This allows you to have fine-grained control over which validation rules are executed.
In summary, by creating a validation context and providing a validator selector, you can trigger validation and apply filters on the rulesets to be executed, giving you the flexibility to execute all the rulesets, just some of them by specifying the ruleset to include or to exclude.
1. Execute all the rules
The code first creates an instance of the “Customer” class with some initial values for its properties. Then, it calls the AutofacConfig.Configure() method to create an Autofac container and starts a new lifetime scope using the BeginLifetimeScope() method.
The code uses the ValidatorOptions.Global.ValidatorSelectors.RulesetValidatorSelectorFactory(includedRuleSets) method to include all the validation rules defined in the validator. The method receives an array of strings, in this case the array contains only one element "*" which means all validation rules.
Then, a new ValidationContext object is created, passing the customer object, the validatorSelector, and the propertyChain.
Finally, it resolves the validator from the container and calls the Validate method passing the customerValidationContext object. The code will print the validation results (whether the validation has passed or not and the messages) in the console using the Console.WriteLine method.
using Autofac;
using FluentValidation;
using FluentValidationExample;
using FluentValidationExample.Models;
using FluentValidationExample.Validators;
var customer = new Customer
{
Name = "Antonio",
Surname = "Esposito",
Address = new Address(),
Status = "Registered"
};
var container = AutofacConfig.Configure();
using (var scope = container.BeginLifetimeScope())
{
var includedRuleSets = new[] { "*" };
var validatorSelector = ValidatorOptions.Global.ValidatorSelectors.RulesetValidatorSelectorFactory(includedRuleSets);
var customerValidationContext = new ValidationContext<Customer>(customer, propertyChain: null, validatorSelector);
var validator = scope.Resolve<IValidator<Customer>>();
var result = validator.Validate(customerValidationContext);
Console.WriteLine($"Validation has passed: {result.IsValid}");
Console.WriteLine("Messages: " + string.Join(", ", result.Errors.Select(error => error.ErrorMessage)));
}The result in the console will be:
Validation has passed: False
Messages: 'Street' must not be empty., 'City' must not be empty., 'Postal Code' must not be empty.
2. Execute just some rule sets specifying the included rule sets
The main difference between this code and the previous one is the way it includes validation rules. This code uses the ValidatorOptions.Global.ValidatorSelectors.RulesetValidatorSelectorFactory(includedRuleSets) method to include the validation rules defined in the ValidationConstants.BasicValidation set, rather than including all validation rules as in the previous code.
using Autofac;
using FluentValidation;
using FluentValidationExample;
using FluentValidationExample.Models;
using FluentValidationExample.Validators;
var customer = new Customer
{
Name = "Antonio",
Surname = "Esposito",
Address = new Address(),
Status = "Registered"
};
var container = AutofacConfig.Configure();
using (var scope = container.BeginLifetimeScope())
{
var includedRuleSets = new[] { ValidationConstants.BasicValidation };
var validatorSelector = ValidatorOptions.Global.ValidatorSelectors.RulesetValidatorSelectorFactory(includedRuleSets);
var customerValidationContext = new ValidationContext<Customer>(customer, propertyChain: null, validatorSelector);
var validator = scope.Resolve<IValidator<Customer>>();
var result = validator.Validate(customerValidationContext);
Console.WriteLine($"Validation has passed: {result.IsValid}");
Console.WriteLine("Messages: " + string.Join(", ", result.Errors.Select(error => error.ErrorMessage)));
}Specifically, the following line is different:
var includedRuleSets = new[] { ValidationConstants.BasicValidation };It creates an array with one element ValidationConstants.BasicValidation which is passed as parameter to the method ValidatorOptions.Global.ValidatorSelectors.RulesetValidatorSelectorFactory(includedRuleSets). This means that only the validation rules defined in the ValidationConstants.BasicValidation set will be executed.
The result in the console will be:
Validation has passed: True
Messages:
3. Execute just some rule sets specifying the excluded rule sets
This code defines a class called RuleSetValidatorSelector which implements IValidatorSelector interface provided by the FluentValidation library. The IValidatorSelector interface defines a single method called CanExecute that is used to determine whether a validation rule should be executed or not.
The class has a constructor that takes an array of strings as input and assigns it to the private variable _excludedRuleSets. This array contains the names of the validation rule sets that should be excluded from the validation process. If the input array is null, the constructor assigns an empty array to the variable.
The class also has an implementation of the CanExecute method. This method takes three inputs:
- An
IValidationRuleobject, which represents the validation rule to be executed. - A string containing the property path, which is used to specify which property the validation rule should be applied to.
- An
IValidationContextobject, which contains information about the current validation process.
The method checks whether the RuleSets property of the validation rule is null or not. If it is null, the method returns true, indicating that the validation rule should be executed. This is the case of validations rule that are not assigned to any RuleSet and hence should be always executed. If it is not null, the method checks if any of the rule sets in the RuleSets property match any of the rule sets in the _excludedRuleSets array. If there is a match, the method returns false, indicating that the validation rule should not be executed. If there is no match, the method returns true, indicating that the validation rule should be executed.
This class can be used as a validator selector when creating a new instance of ValidationContext. The CanExecute method will be called for each validation rule, and it will decide whether the validation rule should be executed or not based on the excluded rule sets passed as parameter to the constructor.
using FluentValidation;
using FluentValidation.Internal;
namespace FluentValidationExample.Validators;
internal class RuleSetValidatorSelector : IValidatorSelector
{
private readonly string[] _excludedRuleSets;
public RuleSetValidatorSelector(string[] excludedRuleSets)
{
_excludedRuleSets = excludedRuleSets ?? Array.Empty<string>();
}
bool IValidatorSelector.CanExecute(IValidationRule rule, string propertyPath, IValidationContext context)
{
return rule.RuleSets == null || !rule.RuleSets.Any(ruleSet => _excludedRuleSets.Contains(ruleSet));
}
}And finally this is how the Program.cs will look like
using Autofac;
using FluentValidation;
using FluentValidationExample;
using FluentValidationExample.Models;
using FluentValidationExample.Validators;
var customer = new Customer
{
Name = "Antonio",
Surname = "Esposito",
Address = new Address(),
Status = "Registered"
};
var container = AutofacConfig.Configure();
using (var scope = container.BeginLifetimeScope())
{
var excludedRuleSets = new[] { ValidationConstants.AddressValidation };
var validatorSelector = new RuleSetValidatorSelector(excludedRuleSets);
var customerValidationContext = new ValidationContext<Customer>(customer, propertyChain: null, validatorSelector);
var validator = scope.Resolve<IValidator<Customer>>();
var result = validator.Validate(customerValidationContext);
Console.WriteLine($"Validation has passed: {result.IsValid}");
Console.WriteLine("Messages: " + string.Join(", ", result.Errors.Select(error => error.ErrorMessage)));
}The result in the console will be:
Validation has passed: True
Messages:
