Mastering .NET FluentValidation: Tips, Tricks, and Best Practices

Pierre Belin
Pierre Belin
Mastering .NET FluentValidation: Tips, Tricks, and Best Practices
Table of Contents
Table of Contents

Step up your .NET development with this expert guide to using FluentValidation. Packed with actionable tips, tricks, and best practices, learn how to seamlessly integrate sophisticated validation logics into your applications for improved performance and reliability.

In our rapidly evolving digital era, ensuring the accuracy and consistency of our application's data is paramount. With the sheer complexity of modern applications, ensuring every piece of data is validated correctly is a challenge. Enter FluentValidation, an exceptional tool designed for .NET developers.

Basics of FluentValidation

FluentValidation is a popular open-source .NET library, specifically engineered to provide a robust mechanism for building strongly-typed validation rules. But why is it so lauded among the developer community? It offers a neat, maintainable, and scalable approach to data validation, combined with a fluent interface that makes rule creation a breeze. The library is regularly updated by a dedicated team of developers, ensuring it aligns with contemporary development standards.

public class GoatValidator : AbstractValidator<Goat>
{
    public GoatValidator()
    {
        RuleFor(goat => goat.Name).NotEmpty().WithMessage("Every goat requires a name.");
        RuleFor(goat => goat.Age).InclusiveBetween(0, 20).WithMessage("Age must be between 0 and 20 years.");
        RuleFor(goat => goat.Color).IsInEnum().WithMessage("This color isn’t recognized.");
    }
}

The RuleFor method is the backbone of FluentValidation. It targets a specific property of the object you're validating and then allows you to chain a variety of validators onto that property. This code ensures that:

  1. Name: Every goat is named.
  2. Age: Their age lies between 0 and 20 years.
  3. Color: Only recognized colors are accepted.

The .WithMessage() method allows you to specify a custom error message to be returned if the validation fails.

Rules can be added up to solve the most complex conditions that the business can represent.

RuleFor(goat => goat.Name)
    .Must(name => name.StartsWith("Young"))
    .When(goat => goat.Age < 3 && goat.Color == GoatColor.White)
    .WithMessage("Young white goats' names should start with 'Young'.");

This rule makes sure that young white goats have names beginning with "Young."

Now that the validator has been created, it's so easy to use that it's almost ridiculous. The Validate method, inherent to the validator, is what brings your validation rules to life.

To begin, instantiate the validator class you've created. For example, if you've built a validator for our goat entities, it'd look something like this:

var newGoat = new Goat { Name = "Jenny", Age = 2, Color = "Green" };

var validator = new GoatValidator();
var validationResult = validator.Validate(newGoat);

The result contains a report of all the models applied to the object to be validated. This includes all rules that have not been respected, with associated error messages.

The IsValid property simplifies the task of checking the validation status:

if (validationResult.IsValid)
{
    // Save the new goat to the database or perform other necessary actions.
}
else
{
    // Handle the validation errors.
    foreach (var failure in validationResult.Errors)
    {
        Console.WriteLine($"Property {failure.PropertyName} failed validation. Error: {failure.ErrorMessage}");
    }
}

In this example, IsValid would return false, prompting the application to handle the validation errors. This provides a user-friendly approach by pointing out precisely where the problem lies, allowing the user to quickly rectify the mistake.

FluentValidation allows you to push the system to the extreme for validation, and apply business rules that tend to translate into a series of innumerable if and else statements. By centralizing a rule in a validator, the very purpose of the rule is highlighted and isolated. This separation also means that the rule can be reused as and when required.

Modularizing the Validator

FluentValidation provides a whole range of features to meet all the validation needs of your application's business, including the most frequently used ones.

The CascadeMode feature in FluentValidation lets you stop rule execution after the first failure for a particular property. You have two choices: either stop at the first mistake, or check all the rules. The advantage of continuing after the first error is that you can retrace all the rules that have not been respected, instead of having to go back to the first error.

public GoatValidator()
{
    RuleFor(goat => goat.Name)
        .Cascade(CascadeMode.Stop)
        .NotEmpty()
        .Must(name => name.StartsWith("Young"));
}

We've touched on this earlier, but it's worth noting that the .When() method offers a great deal of flexibility in setting conditions for validation rules:

RuleFor(goat => goat.Name)
    .NotEmpty()
    .When(goat => goat.Age > 1);

It can also be applied to a set of rules to simplify writing.

When(goat => goat.Age > 1, () => {
   RuleFor(goat => goat.Name)
    .NotEmpty()
});

You might also want to transform a value before validating it. If input properties are not in the expected format, they can be processed upstream before being validated, to avoid creating temporary objects just for this purpose. FluentValidation provides the .Transform() method for this:

RuleFor(goat => goat.Name)
    .Transform(name => name.Trim())
    .NotEmpty();

You can specify rules that should only be run after another rule successfully validates. In the example below, the name rule will only be run if the age rule passes.

RuleFor(goat => goat.Age)
    .GreaterThanOrEqualTo(0)
    .DependentRules(() =>
    {
        RuleFor(goat => goat.Name).NotEmpty();
    });

If you have related entities and want to maintain a separate validator for each but sometimes validate them together, you can do so:

public class FarmValidator : AbstractValidator<Farm>
{
    public FarmValidator()
    {
        RuleFor(farm => farm.OwnerName).NotEmpty();
        RuleFor(farm => farm.Goat).SetValidator(new GoatValidator());
    }
}

Sometimes, the built-in validators might not suffice. In such cases, you can write your own custom validators:

RuleFor(goat => goat.Name).Custom((name, context) =>
{
    if (name == "Billy" && context.InstanceToValidate.Age > 5)
    {
        context.AddFailure("Billy is a young goat's name!");
    }
});

Here, we've set a custom rule that flags a validation failure if a goat named "Billy" is older than 5 years. The Custom method provides flexibility by allowing you to define your own rules.

These are not the only functions available, and to find out about the others, you'll find a link to the documentation at the end of the article.

Summary

Through FluentValidation, the .NET framework offers developers an invaluable toolset that simplifies and strengthens data validation tasks. Our deep dive into its functionality—using the tangible example of goat management—has underscored its efficacy. From the staple validation rules to the more nuanced conditional checks, every feature ensures that data discrepancies are identified and addressed promptly.

In the real-world scenarios of software development, where every piece of data matters, integrating FluentValidation can be the difference between smooth operations and unforeseen complications. Whether you're overseeing a herd of goats or a complex application, this library is pivotal in safeguarding data consistency and trustworthiness.

To go further:

FluentValidation — FluentValidation documentation

Have a goat day 🐐



Join the conversation.

Great! Check your inbox and click the link
Great! Next, complete checkout for full access to Goat Review
Welcome back! You've successfully signed in
You've successfully subscribed to Goat Review
Success! Your account is fully activated, you now have access to all content
Success! Your billing info has been updated
Your billing was not updated