The biggest problem with FluentValidation

Spoiler: it allows invalid objects to exist

Oleksandr Redka
C# Programming

--

In this article, I’ll review commonly brought-up benefits of FluentValidation and present arguments against them. We will start by reviewing the positive sides of fluent validation and then transition to things I’m not so happy about. I’m going to focus specifically on the model validation. Let’s keep domain validation for the future.

What do you need to know?

  1. SOLID principles — some of the benefits of FluentValidation refer to SOLID principles as justification. I will show examples of violations of some principles for the sake of following the others.
  2. Domain-Driven Design — some of the arguments offered will abstract from the source code itself and refer to your Domain to seek answers.

FluentValidation Overview

Example

The main feature of the FleuntValidation package is a nice and shiny Fluent API to define validation rules for classes. Here is an example from the docs website:

// source: https://docs.fluentvalidation.net/en/latest/
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(x => x.Surname).NotEmpty();
RuleFor(x => x.Forename).NotEmpty().WithMessage("Please specify a first name");
RuleFor(x => x.Discount).NotEqual(0).When(x => x.HasDiscount);
RuleFor(x => x.Address).Length(20, 250);
RuleFor(x => x.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
}

private bool BeAValidPostcode(string postcode)
{
// custom postcode validating logic goes here
}
}

And it truly extends your validation rules with amazing readability. You can immediately see all of the rules associated with this class.

Also, they have an integration with AspNetCore that allows you to wire up validation into request middleware and reject requests with invalid models.

Swagger Integration

The existence of validation rules being expressed as an object model adds another layer of functionality that can be built on top of it. For example, this package: MicroElements.Swashbuckle.FluentValidation could analyze validators and populate schema into the OpenApi specification of your AspNetCore App.

Now, those are clear benefits, so what I’m upset about? Well…

The Problem

FluentValidation implies that your validation logic is disconnected from your model classes.

When articles online are advertising for the usage of fluent validation, they are bringing several benefits that this package would give. I want to reiterate those claims and show how the separation of model and validation is rather detrimental to the same aspects of code quality.

Maintainability

By keeping the validation logic separate, the code becomes easier to maintain and understand. Changes to the validation rules won’t affect the model classes and vice versa.

In this example, I’m failing to understand the benefit of this separation. It feels like we are separating them for the sake of separation itself, rather than achieving specific goals.

From practical experience, software engineers will forget to change validation after they’ve changed the model if they are not located together. It’s especially painful for new people on the project who never worked with FluentValidation before.

Also, I see that this separation is justified by the Single Responsibility Principle. And I can’t agree less with that. Unfortunately, the subjective definition of “Responsibility” is at its extreme here, diminishing the purpose of the OOP itself.

Object Oriented Programming was invented because data and behavior naturally belong to each other. That’s what classes are: data and operations with the data. So, whenever we have the behavior-less model and a different class that knows how to perform operations on that data — we are no longer leveraging OOP. We are taking a step back to a procedural code.

Consequently, if we want to refer to SOLID, we are in fact violating the Dependency Inversion principle. Now, we have a tight coupling between classes: the Validator class references the exact type of the model. At the same time, when we were supposed to have strong coupling: data and rules to validate it — we had loose coupling. Data lives in the model class and “someone from outside” should know how to validate it.

And a last pain point, which tbh is more of a neat peak from me, is relying on somebody to execute the validation for you. Just by having an instance of the model class, you can’t know if it was already validated or not. Usually, you have some sort of middleware like MediatR or AspNetCore that would run validation and fail. Then, you need to be sure that all validators are registered to the IoC container and that they can be instantiated. All of this for me sounds more like an extra effort that can be easily avoided.

Reusability

Validation rules might need to be reused across different parts of an application or even across different applications. Having them in a separate class makes this reusability possible.

A similar story goes on here. Having FluentValidation living in a separate class would require any other part of our system to execute those rules on top of the instantiated model.

Contrary, if a model can’t be created with invalid values — it’s no longer possible to bypass our validation rules and so all parts that are instantiating our model will be guaranteed to have only valid instances.

Testability

It’s easier to write unit tests for smaller, well-defined pieces of functionality. If the validation logic is mixed with the model, it can make the unit tests more complex and harder to write.

In some sense, trying to reduce the concept of “Unit” in unit testing to a single line of code is as misguided, as the overuse of the Single Responsibility Principle.

Let me share a piece of code that is supposed to “Unit-test” a validator:

//source: https://www.bytehide.com/blog/fluent-validation-net-core-advantages-disadvantages
public class CustomerValidatorTests
{
private readonly CustomerValidator _validator = new CustomerValidator();

[Fact]
public void Should_Fail_When_Name_Is_Empty()
{
var customer = new Customer {
Name = string.Empty,
Email = "email@example.com",
Password = "password",
Address = "100 Main st."
};

var result = _validator.Validate(customer);

Assert.False(result.IsValid);
}
}

What we can see here, is that our testing becomes more complex.
Now, instead of testing the model and subsequent value-objects, we have to instantiate a model and manually run a validation.

Side note: The fact that the model uses auto-properties allows developers to extend the model, forget about validation rules, and make 0 tests fire about that.

If, on the other hand, when our validation is done inside a model constructor, our test would be as simple as a single line of code:

public class CustomerModelTests 
{
[Fact]
public void Should_Fail_When_Name_Is_Empty()
{
Assert.Throws<WhateverExceptionYouCare>(() => new Customer(name: string.Empty,
email: "email@example.com",
password: "password",
address: "100 Main st."))
}
}

More than that, any changes in the model schema will change a model constructor, thus you will detect all broken dependencies during a compilation. Before you can start running your tests.

Flexibility

Different scenarios might require different validation rules for the same model. By separating the validation rules from the model, you can apply different sets of rules as needed.

If your model requires two sets of validation rules to be applied at different points in time — you need two models.

This might be a clear example of over-reusability. When two business use cases are related to similar sets of input data, it might be tempting to reuse the model that you already have and then define a different set of validation rules. However, different business use cases will most likely diverge as time goes by, and at the end of the day, it might be cheaper to just copy your model. Then, when change or extension comes, you just modify one model without affecting other parts of the system.

For example, in an e-commerce application, adding a product to a cart and adding a product to a wishlist might seem like they can be managed by a single model that will hold an ID of the product.

public record ProductIdModel(Guid ProductId);

However, if we introduce multiple wishlists — this model will diverge in two: adding a product to a cart with an ID and adding a product to a wishlist extended by a Wishlist ID.

public record AddProductToCartModel(Guid ProductId);
public record AddProductToWishlistModel(Guid ProductId, Guid WishlistId);

Regarding discussion of code duplications, I prefer to stick to “The Rule of Three”:

Reusable component should be tried out in three different applications before it will be sufficiently general to accept into a reuse library.

Given this rule, I would stick to copy-pasting models until I have enough evidence that they are in fact the same thing.

Workarounds

Guard Classes

An amazing example of model validation is guard classes. Here is The Link that describes simple extension methods defined around ArgumentException.

You can implement similarly styled static classes concerning your needs and custom exception handling defined on a project.

this.ValidWhen() Extension

Here is a sample extension that allows us to define and execute rules directly in the model constructor, thus solving the concerns expressed above.

public static class FluentValidaitonExtensions
{
public static void ValidWhen<T>(this T instance, Action<InlineValidator<T>> configure)
{
var validator = new InlineValidator<T>();
configure(validator);

validator.ValidateAndThrow(instance);
}
}

public class MyFancyModel
{
public MyFancyModel(string name, DateTime dateOfBirth)
{
Name = name;
DateOfBirth = dateOfBirth;

this.ValidWhen(validator =>
{
validator.RuleFor(model => model.Name)
.Equal("Oleksandr")
.WithMessage("Seems like you are not Oleksandr :(");
validator.RuleFor(model => model.DateOfBirth)
.GreaterThan(DateTime.UtcNow)
.WithMessage("Time-travellers are not allowed.");
});
}

public string Name { get; }
public DateTime DateOfBirth { get; }
}

Summary

Software Engineers are arguing about yet another thing

Fluent Validation is an amazing piece of software that requires careful consideration regarding its use in the form the majority of internet resources present it.

To have a rule of thumb on this matter, I propose a simple thought experiment you can perform on any of your models. Ask yourself:

Do I need for this class instance to exist without being validated?

If your answer is “No” — your choice is validation inside a constructor.

References

Classic Fluent Validation usage examples:

Software and OOP:

--

--