In the previous article, we had a brief look at how to use FluentValidation in an ASP.NET Core application, as an alternative to using Data Annotations. We’ve created a simple API and demonstrated the basic concepts of FluentValidation. So, in this article, we are going to go a bit deeper and focus more on different validators with FluentValidation, covering the types of scenarios we’re likely to hit as developers.

This time, the concepts we’ll go over can be applied to any .NET application, not just ASP.NET Core applications. To demonstrate this, we’re going to be building all our code in a .NET Core class library, but pulling that into an ASP.NET Core API to demonstrate the end result.

To download the source code for this article, you can visit the Different Validators with FluentValidation repository.

This article is divided into the following sections:

Project Setup

Let’s start by opening up Visual Studio 2019, and creating a new Class Library (.NET Core) C#:

Adding a new .NET Core Class Library

Next, let’s go ahead and install FluentValidation via the Package Manager Console:

PM>> install-package FluentValidation

Now, let’s add a new OrderStatus enumeration to the project:
public enum OrderStatus
{
    Accepted,
    Processing,
    Complete
}

Furthermore, we are going to add a class called Product:
public class Product
{
    public string Name { get; set; }
}

Next, let’s add a class called Order:
public class Order
{
    public string Name { get; set; }
    public int Price { get; set; }
    public string CustomerEmail { get; set; }
    public OrderStatus OrderStatus { get; set; }
    public Product Product { get; set; }
}

Finally, let’s set up our validator:
public class OrderValidator : AbstractValidator<Order>
{
    public OrderValidator()
    {
    
    }
}

ASP.NET Core API

To test things out, we are going to add a new ASP.NET Core API project and reference the class library.

Let’s select “Add > New Project” from the Solution Explorer, and select “ASP.NET Core Web Application”:

Adding a new ASP.NET Core Web Application

Let’s accept all the defaults, and then we end up with the following solution structure:

Project Structure for the validators with FluentValidation implementation

To continue, we have to add a reference to our ClassLibrary1 project from WebApplication1:

Adding a reference to our validators project

Let’s right-click on WebApplication1 and select “Set as Startup Project” to make our API the default startup project:

Validators with FluentValidation - Setting an API As a Startup Project

Right after that, we are going to hit the “CTRL-F5” shortcut to start our API without debugging, and we should see the familiar weather forecast result displayed in our default browser:

Sample API Response

Setting up the Controller

Now that we have a basic ASP.NET Core API setup with our project referenced, we need to add a controller to accept an Order object.

To do that, let’s right-click on the Controllers folder and select Add > Controller, and select “API Controller – Empty” from the dialog:

Adding a new Controller to facilitate our validators

Let’s name the controller OrdersController, so it represents the resource we are going to interact with.

In the created OrdersController, let’s add a using statement to reference our ClassLibrary:

using ClassLibrary1;

Next, let’s add the following method:
[HttpPost]
public ActionResult Post([FromBody] Order order)
{
    return Ok("Success!");
}

This method is identical to our previous article, in which we are simply accepting an object in the request body from a POST request, and returning a 200 (OK) response with the text “Success!”.

Wiring up FluentValidation

Now, we need to add the FluentValidation ASP.NET middleware. To do that, let’s open up the Package Manager Console and install a new project to our WebApplication1:

PM>> install-package FluentValidation.AspNetCore

Then, let’s open up Startup.cs and add the necessary using statements:
using ClassLibrary1;
using FluentValidation.AspNetCore;

Additionally, we have to register the OrderValidator class in the ConfigureServices method:
services
    .AddControllers()
    .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<OrderValidator>());

This will register the OrderValidator class we created previously with FluentValidation, and make sure FluentValidation runs on all requests to our API.

Let’s open up Postman and make sure everything is working, by adding the POST request:

Postman request - Testing validators with FluentValidation

If we hit the Send button, we should see a 200 (OK) status returned and the “Success!” message:

Postman response - Testing validators with FluentValidation

Now that we have everything set up, we are going to add validation to our models and test it out against our API as we go.

Built-in Validators

FluentValidation has a number of built-in validators, which saves us writing code to perform the most common validation. Let’s go over a few examples.

Null & Empty Validation

The most common and simple validation is ensuring value is set, and in the case of string or sequences, ensuring the value is not empty.

Let’s add the following two rules to our OrderValidator:

RuleFor(model => model.CustomerName).NotNull();
RuleFor(model => model.CustomerEmail).NotEmpty();

These two rules ensure a value is present for CustomerName, and we have a non-whitespace value for CustomerEmail. In most cases, you’d just use the NotEmpty() validator, unless you want to allow whitespace as a valid value, however, it’s worth showing both options.

Length Validation

After we’ve ensured our strings contain a non-whitespace value, the next validation we would normally like is around the length.

Let’s amend the rule we just created for CustomerName:

RuleFor(model => model.CustomerName).MinimumLength(10);

Now we are ensuring the name is at least 10 characters long. If we prefer, we can make use of the following similar length validators:

  • MaximumLength (useful if we want to ensure a string is less than or equal to a specified number of characters)
  • Length (when we want the string to be exactly a certain number of characters)

Range Validation

Generally, whenever we have integers on our model, we need to apply some kind of reasonable bounds over the value. This is where we can make use of the range validators.

Let’s get back to the OrderValidator class and add a validator against our Price property:

RuleFor(model => model.Price).InclusiveBetween(1,1000);

Here we have simply ensured the value of the price is between 1-1000 inclusive. We can also use the following similar length validators whose usage is thankfully obvious due to the naming:

  • ExclusiveBetween
  • LessThan / LessThanOrEqualTo
  • GreaterThan / GreaterThanOrEqualTo

Email Validation

Another commonly required validation is against email addresses. All we need to do to enable this is to add the following rule against our Email property, replacing the NotEmpty() rule we had previously:

RuleFor(model => model.CustomerEmail).EmailAddress();

The EmailAddress() validator behind the scenes makes use of regular expressions (which is another built-in validator). This very handy validator covers all the various email formats and edge cases so that we don’t need to.

Enum Validation

Most .NET applications make use of enumerations to signify the state of an object. Often painfully when validating these, we need to do casting/boxing/unboxing and check all the various members of the enum to see if the input is valid.

With FluentValidation it’s extremely easy. To demonstrate that, let’s add enum validation against our Status property:

RuleFor(model => model.OrderStatus).IsInEnum();

The IsInEnum() method will ensure the value is one of the included members of the enum.

While it may seem simple here, consider the scenario of an API like in the previous article. In that scenario, if we had an enum on our model we would accept the value from the calling application in the form of an int or string, which would then be model-bound to our enum property. Since we have no control over these input values, we need to apply a ‘whitelist-style’ approach to the enum validation to ensure the bound value is what we allow. This is exactly what the IsInEnum() method does for us.

Let’s send the same request in Postman we had previously and see the result:

Postman response - enum validators with FluentValidation

Now, we see we are receiving validation errors for the rules we created, which means everything is working well.

Let’s change the body of the request to make everything succeed again:

Postman response - Successful enum validation with FluentValidation

We’ve now covered most of the built-in validators we’d normally make use of. In the next section, we’ll discuss how to chain validators together.

Chaining Validators

Often we need to apply more than one validation rule to a particular property. That’s where chaining comes in.

Let’s demonstrate this by chaining a validator to our existing EmailAddress() validator:

RuleFor(model => model.CustomerEmail)
    .EmailAddress()
    .MinimumLength(20);

Here we are ensuring two things:

  1. The value of CustomerEmail is a valid email address
  2. The value of CustomerEmail is at least 20 characters

It’s quite a contrived example, but it demonstrates a technique we can apply when we want to make use of a built-in validator, but it doesn’t quite meet our needs and we want to be more specific.

Let’s modify the value of customerEmail in the Postman request to “AAAAA” and hit Send:

Postman response - Chaining validators with FluentValidation

Notice we are now receiving 2 validation errors, for each of our chained rules.
Here it’s also worth mentioning how the “cascade” mode of FluentValidation works. In the above example, the EmailAddress() validator is applied, and regardless if the validation succeeds or fails, the MinimumLength() validation is then also applied.

This has the side effect of applying extra validation (and therefore processing) when the call is going to fail anyway. Sometimes this is not desired.

To prevent this, let’s change the email validation rule:

RuleFor(model => model.CustomerEmail)
    .Cascade(CascadeMode.StopOnFirstFailure)
    .EmailAddress()
    .MinimumLength(20);

Now, if the EmailAddress() validation fails, the MinimumLength() validator is not executed. The Cascade method can also be applied to the entire validator in the constructor, or globally to all validators.

Let’s run the app and hit the same request in Postman again:

Postman response - Cascade options for validators with FluentValidation

Now only the first validation rule is displayed. Let’s change the value of customerEmail back to “[email protected]” so our input is valid again.

In the next section, we’ll discuss how we can use nested validators if we have multiple classes.

Nested Validators

In our current project, we have two classes Order and Product. So far, we’ve only applied validation to our Order class. What if we wanted to apply validation to our Product class also? How would we then ensure that validation is called when we validate the Order? This is a common scenario, so let’s see how to implement it.

First, let’s add a simple validator for our Product class:

public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(model => model.Name).NotEmpty();
    }
}

Let’s keep it simple here because we want to focus on how to call this validator from the Order class.

That said, let’s add a new rule to our Order validator:

RuleFor(model => model.Product)
    .NotNull()
    .SetValidator(new ProductValidator());

Very simply, we are ensuring value for Product is sent, and handover validation to the ProductValidator. This is a great example of separation of concerns, as if/when the Product class evolves (and therefore, the validation rules), it’s not a concern of the OrderValidator.

Let’s run our API and execute the existing request in Postman:

Postman response - Erroring nested validators with FluentValidation

Now, we receive an error for the product property, specifying it needs to exist. This is our NotNull() validation rule firing.

Let’s try changing the value of the product to an empty object:

Postman response - Nested validator error with FluentValidation

We are still receiving an error, but notice it’s now the nested validation rule firing for the Product.Name property.

To fix the input, let’s change the input again:

Postman response - Successful nested validation with FluentValidation

We can see the request is now succeeding.

Next, let’s talk about how we can validate collections.

Collections

In our current project, a single Order has a single Product. But what if a single order has many products? (a more realistic scenario).

Let’s update our Order class to reflect that:

public Product[] Products { get; set; }

If we jump over to our OrderValidator, we now see that we have a build error on the following line:
RuleFor(model => model.Product)
    .NotNull()
    .SetValidator(new ProductValidator());

What we’d like to do now is still make use of our ProductValidator, but invoke it for each product.

To do that, we are going to modify the erroring line:

RuleForEach(model => model.Products).SetValidator(new ProductValidator());

Now, we ensure that each product is validated against the ProductValidator rules.

Let’s run the app, and modify our Postman request:

Postman response - Collection validators with FluentValidation

Notice how we have two products in the input, but validation is failing for the second (index 1) product because the “name” property is empty. This proves validation is being executed for each item in the array.

Let’s fix up the Postman request:

Postman response - Successful collection validation with FluentValidation

Now everything is working again.

In the next section, we’ll talk about how to throw validation exceptions.

Throwing Exceptions

When it comes to invoking our validators, the most common way is by using the Validate() method, and then interrogating the ValidationResult().

However, what if we want to automatically throw an exception and short-circuit the code?

We can do that by using the ValidateAndThrow() method:

orderValidator.ValidateAndThrow(order);

Notice this time we’re not capturing the validation result. This is because ValidateAndThrow returns void. Either the validation succeeds and the code continues, or a ValidationException is thrown.

The most common reason we do this is due to global error handling. Instead of constantly checking for validation results all over our code and acting appropriately (which would violate the “DRY” principle), we can instead throw a ValidationException and catch that exception higher up in our code.

Default Behavior

In our API this behavior is in fact happening implicitly with the FluentValidation.AspNetCore middleware. In other words, we don’t need to call Validate() or ValidateAndThrow(), as this happens automatically.

But to demonstrate the behavior, let’s amend our existing API method:

[HttpPost]
public ActionResult Post([FromBody] Order order)
{
    var product = new Product
    {
        Name = null // will fail validation
    };
    var validationResult = new ProductValidator().Validate(product);
    return validationResult.IsValid
        ? (ActionResult) Ok("Success!")
        : BadRequest("Validation failed");
}

Let’s explain our code:

  1. First, we create a new Product with values we know will fail validation
  2. Next, we create a ProductValidator() and validate the product
  3. Finally, we return Ok or BadRequest depending on the validation result

If we run our API and execute the existing request in Postman:

Postman response - Default validation behavior with FluentValidation

We can see the response “Validation failed”.

Using ValidateAndThrow()

Let’s now change our code to throw instead. First, let’s add a reference to FluentValidation:

using FluentValidation;

Next, let’s modify our method:
[HttpPost]
public ActionResult Post([FromBody] Order order)
{
    var product = new Product
    {
        Name = null // will fail validation
    };
    new ProductValidator().ValidateAndThrow(product);
    return Ok("Success!");
}

Now let’s run our API and execute the request in Postman:

Postman response - ValidationException with FluentValidation

Notice this time, the response code is 500 (Internal Server Error), and the response shows that a ValidationException was thrown.

This isn’t a great response payload for a bad request. This is why the FluentValidation.AspNet middleware is preferred, as it “catches” this exception, unpacks the validation errors, and returns a 400 (Bad Request) with a nice response model.

However, if we were on other flavors or platforms of .NET or wanted different behavior, we would want to imitate this behavior in the form of a try/catch statement.

Let’s revert our method back to the previous section:

[HttpPost]
public ActionResult Post([FromBody] Order order)
{
    return Ok("Success!");
}

In the final section, we’ll talk about how to take FluentValidation to the next level in the form of custom validators.

Custom Validators

So far, we’ve mostly had FluentValidation take care of all the hard work for us. What if we had very specific logic that wasn’t covered by the built-in validators with FluentValidation? That’s where we can make use of custom validators. In fact, if you right-click on any built-in validator we’ve used so far, and select “Go to definition”, you’ll see it exists in a class called DefaultValidatorExtensions. These validators extend the IRuleBuilder interface to perform validation.

Let’s take a look at our existing validation for the CustomerName property on our Order class:

RuleFor(model => model.CustomerName).MinimumLength(10);

What if we want to do more than ensure it’s at least 10 characters? What if we want to make sure it’s actually a real name with a first and surname, with a space in the middle? Let’s build a custom validator to encapsulate that logic.

Adding a Custom FullName Validator

First, let’s add a FluentValidationExtensions class with a new extension method:

public static class FluentValidationExtensions
{
    public static IRuleBuilderOptions<T, string> FullName<T>(this IRuleBuilder<T, string> ruleBuilder)
    {
        return ruleBuilder
                   .MinimumLength(10)
                   .Must(val => val.Split(" ").Length >= 2);
    }
}

Let’s explain our code:

  1. Firstly, we can see that there are 2 type parameters on the method signature, T and string. Essentially this means that the custom validator can be applied to any validator of a class, as long as the property being validated is a string. If we want to restrict it to certain classes, we could make use of C# generic constraints, but we keep it simple for demonstration purposes.
  2. In the implementation, we use the existing MinimumLength validator, and add the validation code:

.Must(val => val.Split(" ").Length >= 2);

We’re making use of “chaining validators” (which we discussed previously), and also making use of the Must validator which accepts a predicate requiring a return type of bool. Essentially, this is where we can put any custom logic. In this case, we’re splitting the value by space into an array and ensuring there are at least two elements (first name, then potentially a middle and last names). Of course, this is a very naive implementation, but again we keep it simple to focus on different validators with FluentValidation and not complicated logic.

Using Our Custom Validator

Let’s modify our validation rule to use the new validator:

RuleFor(model => model.CustomerName).FullName();

Now, let’s run our API again and execute the Postman request:

Postman response - Custom validators with FluentValidation

Our request still succeeds.

Let’s see what happens if we change the value of customerName to “JoeBloggs” (no space) and execute the request again:

Postman response - Failing custom validators with FluentValidation

Notice both our validation rules failed. Also, we can see the second validation rule isn’t very user friendly, so let’s amend the custom validator and make use of the WithMessage() method:

return ruleBuilder
           .MinimumLength(10)
           .Must(val => val.Split(" ").Length == 2)
           .WithMessage("Name must contain a single space and be at least 10 characters long");

This should make the error a bit easier to understand. Let’s run our API and execute the request again:

Postman response - Custom error messages in validators with FluentValidation

Now the client should be able to easily understand why their request failed, and make the necessary adjustments. The WithMessage() method is useful for this purpose when we want to provide better error information that FluentValidation provides by default, which is especially important when building a public API.

Now that we’ve seen how to write custom validators, let’s wrap things up.

Conclusion

In the previous article, we discussed the basics of FluentValidation and how to get a simple ASP.NET Core API up and running. In this article, we went down the rabbit hole and covered many scenarios we’d usually need to deal with when validating objects in a .NET application.

For most companies, our data is our most valuable asset so it’s important we protect what goes into our system. That’s why it’s essential to be equipped with a great library like FluentValidation and know which rules to apply to what scenarios. Hopefully, after this article, you’re now well skilled in these situations.

Happy validating and remember.. “never trust user input!”.