Validating data with FluentValidation
Introduction
Validating data is as old as computer science itself and is necessary to build robust software systems. This article will demonstrate how the c# library FluentValidation can be used.
Validation
The software to validate is preferrably not a system or module as a whole, but rather smaller pieces of code like objects.
The validator should be responsible for the validation of your objects. The code invoking the validator can decide what to do based on the validation result. This separation of concerns keeps the code clean.
FluentValidation
The C# library FluentValidation is exactly what can be applied here. As the name suggests, it uses a fluent-style syntax which improves readability. The rest of the article will show in more detail how FluentValidation can be used in different scenarios.
Simple Example
An example is the validation of a simple MoneyTransaction
class and its validator.
public class MoneyTransactionValidator: AbstractValidator<MoneyTransaction> {
public MoneyTransactionValidator() {
RuleFor(x => x.Amount).NotEmpty().WithMessage("Amount must be there");
RuleFor(x => x.BankAccountFrom).NotEmpty().WithMessage("Empty from bank account");
RuleFor(x => x.BankAccountTo).NotEmpty().WithMessage("Empty to bank account");
RuleFor(x => x.CurrencyCode).Must(BeAThreeCharacterString).WithMessage("CurrencyCode is not 3 characters");
RuleFor(x => x.TimeStamp).Must(BeAValidTimeStamp).WithMessage("Incorrect timestamp");
RuleFor(x => x).Must(HaveADescriptionWhenAmountBiggerThan1000).WithMessage("Amounts bigger than 1000 should have a description");
}
private bool BeAThreeCharacterString(string currencyCode)
{
return (currencyCode?.Length == 3) && (currencyCode?.All(Char.IsLetter) == true);
}
private bool BeAValidTimeStamp(string timestamp) {
DateTime parsedDate;
return DateTime.TryParseExact(timestamp, "yyyyMMdd",
CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDate);
}
}
MoneyTransaction moneyTransaction = GetMoneyTransactionExample();
var validator = new MoneyTransactionValidator();
var result = validator.Validate(moneyTransaction);
In the constructor the rules are defined. With the RuleFor function and lambdas the inbuild validation functions can be used. Are more custom functions needed? Look in the code above how the validation of the properties CurrencyCode
and TimeStamp
are implemented. The result object is an instance of the ValidationResult
class, and contains among others the boolean IsValid property and information about possible errors.
Data validation vs Business rule validation
Data validation validates the format and structure of the data and can be executed relatively in isolation. Business rule validation tends to be more complex and usually requires data from related objects.
FluentValidation matches great with data validation. For example, an email adress should be in a certain format. A datetime must be in a valid format. But at some point it turns into business rule validation. A description of 250 characters long is normally a data validation, but what if for currency USD only 100 characters are allowed? Or what if validation is needed to check if the currency is on a list of banned currencies. Or what if the account balance is too low? In the latter two cases it’s definitely business rule validation. It’s good practice to think this over before implementation is started.
It’s up to yourself where to implement business rule validation. If FluentValidation is chosen it can be implemented in a validator; e.g. with a different ruleset or as a separate business rule validator.
Validation on more than one property
Though FluentValidation suits best for validating single properties, complex validation on a group of related properties is also possible.
The FluentValidation documentation shows property-validation dependent on the value of another property is possible using the inbuilt .When()
function:
public MoneyTransactionValidator() {
..
RuleFor(x => x.Description).NotEmpty().When(x => x.Amount?.Length > 3).WithMessage("Amounts bigger than 1000 should have a description");
..
}
When this rule is extended it might become more difficult to read. The validation can be simplified by by moving the code in a separate method like this:
public MoneyTransactionValidator() {
..
RuleFor(x => x).Must(HaveADescriptionWhenAmountBiggerThan1000).WithMessage("Amounts bigger than 1000 should have a description");
..
}
private bool HaveADescriptionWhenAmountBiggerThan1000(MoneyTransaction moneyTransaction)
{
//the whole moneyTransaction object is available, so all properties can be used here.
if (moneyTransaction.Amount?.Length > 3)
{
return string.IsNotNullOrEmpty(moneyTransaction.description);
}
return true;
}
Note that the lambda passed to the RuleFor function RuleFor(x => x)
is making sure that the method HaveADescriptionWhenAmountBiggerThan1000
receives the full MoneyTransaction object, so that all properties are accessible.
How to pass data to the validator?
There are situations extra data is needed for the validation. Because of reusability and responsiblity reasons it’s desired this data can be passed to the validator.
The validator can access this information in several ways:
- Passing it in via context. This is the way described in the FluentValidation documentation.
- Inject an interface of a service/repository in the constructor.
- Other ways like invoke a service or repository/database on a concrete object, passing data via properties, put the data inside the validator class etc.
The first two options refrain from maintaining state and are the ones discussed below.
Suppose the currency of the MoneyTransaction
needs to be validated against a list of valid currencies, which needs to be passed to the validator. Using a context variable it can be done as follows:
public class MoneyTransactionValidator: AbstractValidator<MoneyTransaction> {
public MoneyTransactionValidator() {
..
RuleFor(x => x.CurrencyCode).Must(BeAKnownCurrencyCode).WithMessage(x => string.Format("Currency {0} is not known", x.CurrencyCode));
}
private bool BeAKnownCurrencyCode(MoneyTransaction moneyTransaction, string currencyCode, PropertyValidatorContext context)
{
//the ParentContext is of type:FluentValidation.ValidationContext<FluentValidationExample.MoneyTransaction>;
//the RootContextData is of type Dictionary<String, Object>;
var currencyArray = context.ParentContext.RootContextData["CUR"] as string[];
return currencyArray?.Contains(currencyCode) == true;
}
}
..
var moneyTransaction = GetMoneyTransactionExample();
var context = new ValidationContext<MoneyTransaction>(moneyTransaction);
string[] currencyArray = { "EUR", "USD", "JPY" };
context.RootContextData["CUR"] = currencyArray;
var validator = new MoneyTransactionValidator();
var result = validator.Validate(context);
//..code for handling the result
This works well, the extra data is passed into the validator, and the method BeAKnownCurrencyCode
can read the incoming data. Passing the data in the RootContextData does however not look very elegant.
Another way is injecting an interface in the constructor.
public class MoneyTransactionValidator: AbstractValidator<MoneyTransaction> {
private ICurrencyService CurrencyService { get; }
public MoneyTransactionValidator(ICurrencyService currencyService) {
this.CurrencyService = currencyService;
..
RuleFor(x => x.CurrencyCode).Must(BeAKnownCurrencyCode).WithMessage(x => string.Format("Currency {0} is not known", x.CurrencyCode));
}
private bool BeAKnownCurrencyCode(string currencyCode)
{
var currencyArray = this.CurrencyService.GetValidCurrencies();
return currencyArray?.Contains(currencyCode) == true;
}
MoneyTransaction moneyTransaction = new MoneyTransaction();
var validator = new MoneyTransactionValidator(new CurrencyService);
validator.Validate(moneyTransaction);
The ICurrencyService interface is injected in the constructor and set to the CurrencyService
property. The method BeAKnownCurrencyCode
now can invoke the CurrencyService
instead of reading from the context.
The Validator class is now ready for use with an IoC container, and the dependency can be mocked away in a unit test. It does make the validator dependent on the interface.
Collection validation
According to the creator Jeremy Skinner “FluentValidation’s validators are designed to interact with a single object with multiple properties.”
This has some implications for validating lists. Given a list of objects the best way to validate is loop through the list and call the validator for each object. Example code would look like this:
var validator = new MoneyTransactionValidator();
var moneyTransActionList = GetMoneyTransactionListExample();
foreach (var moneyTransaction in moneyTransActionList)
{
var result = validator.Validate(moneyTransaction);
//.. code for handling the result
}
Another way is to create a list validator like this:
public class MoneyTransactionListValidator: AbstractValidator<List<MoneyTransaction>> {
public MoneyTransactionListValidator()
{
RuleFor(x => x).SetValidator(new MoneyTransactionValidator());
}
}
Creating a Collection validator method returning ValidationResult.
There are however situations where some special validation on a list is needed. There could be a business requirement which require checking the whole list. E.g. validate that at least 30% of the transactions are in currency EUR. Or a more complex situation where more flexibility is needed.
public ValidationResult ValidateMoneyTransactionList(List<MoneyTransaction> moneyTransactionList)
{
var result = new ValidationResult();
foreach(var moneyTransaction in moneyTransactionList)
{
//..some validation code here, e.g. based on the total list
//or just to be more flexible
//if (there is a validation error)
//{
var ValidationFailure = new ValidationFailure(".", "Not enough EUR", moneyTransaction);
//ValidationFailure.CustomState = anotherObjectWithInformation;
result.Errors.Add(ValidationFailure);
//}
}
return result;
}
var validator = new MoneyTransactionListValidator();
var moneyTransActionList = GetMoneyTransactionListExample();
var result = validator.ValidateMoneyTransactionList(moneyTransactionList);
foreach(var validationFailure in result.Errors)
{
var moneyTranaction = validationFailure.CustomState as MoneyTransaction;
//..
//logic for handling error
}
In the example above the custom method ValidateMoneyTransactionList
is implemented in the list validator. The method accepts a moneyTransactionList as an input parameter and returns a ValidationResult
. This is the same FluentValidation class which is returned by default.
This time there is complete freedom how to create it. Note that optionally the moneyTransaction
object can be put in the ValidationFailure object. Also the CustomState
property can be used to return information.
To conclude
FluentValidation supports data validation and ensures separation of concerns with the validation code being in a separate class. Validators can be built dependent on different properties, passing data to the validator is possible, and it’s customizable to support e.g. list validation. These features open ways for more complex and business rule validation.