Grouping and Reusing Validation Rules in FluentValidation
In previous posts about FluentValidation, we explored basic and complex validation rules that allow precise input data checking in our applications. As we discussed, advanced validations are crucial in complex systems where data is closely interconnected and requires thorough verification.
Today, we will look at two important mechanisms in FluentValidation that further streamline the validation process — RuleSet and Include. The RuleSet mechanism enables the grouping of validation rules, which is extremely useful when we want to apply different sets of rules depending on the context. Meanwhile, the Include mechanism allows for the reuse of existing validation rules, significantly simplifying the code and enhancing its readability.
Both RuleSet and Include are tools that help manage the complexity of validation by enabling more modular and reusable code. They allow us to create more flexible and scalable solutions that are easier to adapt to changing business requirements.
Grouping Rules with RuleSet
RuleSet in FluentValidation is a mechanism that allows for grouping and organizing validation rules into logical sets. This makes it easier to manage validation, especially in situations where different operations on the same object require different validation rules. Using RuleSet is crucial for maintaining code readability and organization, as well as ensuring flexibility in the validation process. The benefits of using RuleSet include:
- Modularity: Rules are grouped into logical sets, making them easier to manage and increasing code readability.
- Flexibility: Different validation rules can be easily applied for different scenarios without code duplication.
- Maintainability: Changes to validation rules for a specific scenario are isolated, making the code easier to maintain and test.
In FluentValidation, validation rule grouping is defined using the RuleSet method, where the name of the set and the set of rules to be applied are specified. For example, you can have separate sets for creating ("Creation") and editing ("Update") an object. Within each RuleSet, validation rules are defined just like in the standard case, but they are only executed when validation is invoked using the appropriate RuleSet.
When RuleSets are defined, we can apply them to validate objects. This is done by calling the Validate method on an instance of the validator, passing the object to be validated and the name of the RuleSet we want to use. In our example, for a new customer, we use the "Creation" RuleSet, and for updating an existing customer, we use the "Update" RuleSet.
What happens if we call the
Validatemethod without specifying aRuleSet? In this case, FluentValidation will execute all validation rules that are not assigned to any specificRuleSet. This means that if we have defined rules outside anyRuleSet, those rules will be applied. This is useful when we have a set of rules that are common to different operations, and we don't want to duplicate them in eachRuleSet.
The naming of
RuleSetshould be intuitive and descriptive, reflecting the specific context or action, e.g.,AccountCreationinstead ofRuleSet1. Use a consistent naming convention likeCamelCase, avoid abbreviations unless they are widely recognized. For sets of rules related to specific functionality, use consistent prefixes, e.g.,ProductCreation,ProductUpdate. Document eachRuleSet, explaining its use and context, and regularly review and update the names to maintain clarity and relevance as the application evolves.
Let's assume we are working on a customer management system, and we need to ensure that the data entered by users is correct. In our system, we have different validation requirements depending on whether the customer is being newly added or updated.
Our starting point is the Customer class, which represents a customer in our system:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
Each customer has an identifier (Id), a name (Name), and an email address (Email).
Next, we create the CustomerValidator, which specifies the validation rules. With the help of RuleSet, we define different sets of rules for different scenarios:
using FluentValidation;
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
// RuleSet for creating a new customer
RuleSet("Creation", () =>
{
RuleFor(customer => customer.Name)
.NotEmpty()
.WithMessage("First name is required.");
RuleFor(customer => customer.Email)
.NotEmpty()
.EmailAddress()
.WithMessage("A valid email address is required.");
});
// RuleSet for updating an existing customer
RuleSet("Update", () =>
{
RuleFor(customer => customer.Id)
.NotEmpty()
.WithMessage("Customer ID is required.");
RuleFor(customer => customer.Name)
.NotEmpty()
.WithMessage("First name is required.");
RuleFor(customer => customer.Email)
.NotEmpty()
.EmailAddress()
.WithMessage("A valid email address is required.");
});
}
}
In Creation, we check that the name and email are not empty and that the email is valid. In Update, we additionally ensure that the customer has a specified Id.
When it comes time to validate, we can easily apply the appropriate RuleSet. The example below shows how to do this:
public void ValidateCustomer()
{
var customerValidator = new CustomerValidator();
var newCustomer = new Customer
{
Name = "John Doe",
Email = "john.doe@example.com"
};
var creationResults = customerValidator.Validate(newCustomer, strategy => strategy.IncludeRuleSets("Creation"));
var existingCustomer = new Customer
{
Id = 1,
Name = "Jane Doe",
Email = "jane.doe@example.com"
};
var updateResults = customerValidator.Validate(existingCustomer, strategy => strategy.IncludeRuleSets("Update"));
if (!creationResults.IsValid || !updateResults.IsValid)
{
foreach (var failure in creationResults.Errors)
{
Console.WriteLine($"Property {failure.PropertyName} failed validation. Error: {failure.ErrorMessage}");
}
foreach (var failure in updateResults.Errors)
{
Console.WriteLine($"Property {failure.PropertyName} failed validation. Error: {failure.ErrorMessage}");
}
}
else
{
Console.WriteLine("Both customers are valid!");
}
}
Many of the methods in FluentValidation are extension methods such as “Validate” above and require the FluentValidation namespace to be imported via a using statement, e.g. “using FluentValidation;”.
In this example, we first create a new customer and validate them using the "Creation" RuleSet. Then, we take an existing customer and validate them using the "Update" RuleSet. If the validation fails, we display the errors.
Looking at our grouping rules, we can notice that the validation rules for Name and Email are repeated in both the "Creation" and "Update" RuleSets. Can this be refactored? Of course, below is an example after refactoring:
using FluentValidation;
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(customer => customer.Name)
.NotEmpty()
.WithMessage("First name is required.");
RuleFor(customer => customer.Email)
.NotEmpty()
.EmailAddress()
.WithMessage("A valid email address is required.");
RuleSet("Creation", () =>
{
// Specific rules for the process of creating a new customer
// You can place rules here that are unique to this process
// I encourage the reader to introduce their own rules.
});
RuleSet("Update", () =>
{
RuleFor(customer => customer.Id).NotEmpty().WithMessage("Customer ID is required.");
});
}
}
In the code above, the validation rules for Name and Email are placed in the global section, which means they will always be applied, regardless of whether we call validation with the RuleSet for creating ("Creation") or updating ("Update"). This avoids rule duplication in different RuleSets.
At the same time, by using RuleSet, we retain the ability to define rules specific to a given context – in this case, requiring the Id field to be filled in the updating process ("Update").
In summary, RuleSet in FluentValidation is a powerful tool that allows for effective grouping and management of validation rules. Its use significantly simplifies maintaining order and clarity in projects where data validation is a crucial element.
Reusing Rules
The Include mechanism in FluentValidation is used to incorporate one validator into another, enabling the creation of modular and reusable validation components. This allows for the composition of more complex validators from simpler, dedicated validators, leading to better code organization and avoiding duplication of validation logic.
Include is used when you want the rules defined in one validator to be part of another validator. This can be understood as "include these validation rules in my current set of rules." It is particularly useful when you have common validation rules that you want to apply in multiple different validation contexts.
In the example, a PersonValidator includes rules from PersonAgeValidator and PersonNameValidator:
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
Include(new PersonAgeValidator());
Include(new PersonNameValidator());
}
}
Let's assume PersonAgeValidator and PersonNameValidator are defined as follows:
public class PersonAgeValidator : AbstractValidator<Person>
{
public PersonAgeValidator()
{
RuleFor(person => person.Age)
.GreaterThan(0).WithMessage("Age must be greater than 0");
}
}
public class PersonNameValidator : AbstractValidator<Person>
{
public PersonNameValidator()
{
RuleFor(person => person.Name)
.NotEmpty().WithMessage("Name is required");
}
}
In this scenario:
PersonAgeValidatordefines validation rules for a person's age.PersonNameValidatordefines validation rules for a person's name.PersonValidatorincludes these two validators, creating a composite validator that checks both the age and the name of a person.
Using Include brings several benefits:
- Modularity: You can define dedicated validators for specific aspects of a model and easily combine them.
- Reusability: Validation rules defined in one place can be reused in multiple validators, reducing code duplication.
- Ease of Managing Changes: Updates in dedicated validators are automatically reflected in all validators that include them.
In summary, the Include mechanism in FluentValidation is a powerful tool for building complex validators in a modular and manageable way, which is crucial for maintaining clean and efficient validation code in larger projects.
In this article, we explored how to use the RuleSet and Include mechanisms in FluentValidation to effectively group and reuse validation rules. With these tools, we can create more modular, flexible, and manageable validation solutions. Moreover, we can combine them to create comprehensive validation rules that are both reusable and tailored to specific scenarios.
See you in the next posts.