Mastering Validation: A Deep Dive into FluentValidation for .NET 8 Applications

Chaitanya (Chey) Penmetsa
CodeNx
Published in
4 min readMar 11, 2024

In this blog we will delve into implementing validations in .NET application using FluentValidation. Traditionally .NET applications used to do validations using Data annotations, but there used to be some limitations with that approach like model or dto classes used to look bloated, not extensible, no control on messages and behavior as well as not flexible for testing. This is where libraries like FluentValidation help solve lot issues.

Image taken from https://github.com/FluentValidation/FluentValidation

What is FluentValidation?

FluentValidation is a library that provides a fluent interface for defining validation rules for objects in .NET. It allows you to define complex validation logic using a clear and expressive syntax.

Purpose

  • Define validation rules for properties of a class in a declarative and readable way.
  • Centralize validation logic for a class, improving code organization and reusability.
  • Support complex validation scenarios such as conditional validation and cross-property validation.

Design Pattern

FluentValidation follows the Fluent Interface design pattern, providing a fluent and expressive API for defining validation rules. It promotes a clear and readable syntax for defining complex validation logic.

When to Use

  • Validate properties of a class, such as DTOs (Data Transfer Objects) or domain models.
  • Implement complex validation rules involving multiple properties or conditions.
  • Centralize and encapsulate validation logic within a dedicated validator class.

Now let us setup a sample API and demonstrate how to use FluentValidation in .NET. In this case let us take a scenario where we have Order API which will create customer as well as create order, in this let us build some validators using FluentValidation. Before doing that, we need to install couple of NuGet packages as shown below.

In business logic project where all the validators and logic stay add below package:

dotnet add package FluentValidation

In the API project add below package:

dotnet add package FluentValidation.AspNetCore

Now let us build some sample Validators for validating Customer, Address and Order.

How to use built-in validators

FluentValidation has several built-in validators, which saves us writing code to perform the most common validation. As shown below we can even chain validation like NotEmpty and MaximumLength.

using BusinessLogic.Models;
using FluentValidation;

namespace BusinessLogic.Validators
{
public class OrderRequestValidator : AbstractValidator<OrderRequest>
{
public OrderRequestValidator()
{
RuleFor(or=>or.ProductName).NotEmpty().MaximumLength(255).WithMessage("Product Name Required for placing order");

RuleFor(or => or.Quantity).GreaterThan(0).LessThan(50);

RuleFor(or => or.PromoCode).NotEmpty().MaximumLength(10);
}
}
}

Nested Validators

We can nest validators inside another validator, as shown below we are calling Order and Address validator inside customer validator.

using BusinessLogic.Models;
using FluentValidation;

namespace BusinessLogic.Validators
{
public class CreateCustomerRequestValidator : AbstractValidator<CreateCustomerRequest>
{
public CreateCustomerRequestValidator()
{
RuleFor(cus => cus.FirstName).NotEmpty().MaximumLength(100);

RuleFor(cus => cus.LastName).NotEmpty().MaximumLength(100);

RuleFor(cus => cus.Email).NotEmpty().EmailAddress().WithMessage("Email not in expected format");

RuleFor(cus => cus.Age).GreaterThan(20);

RuleFor(cus => cus.PhysicalAddress).NotNull().SetValidator(new AddressRequestValidator());

RuleFor(cus => cus.MailingAddress).NotNull().SetValidator(new AddressRequestValidator());

RuleFor(cus => cus.Order).NotNull().SetValidator(new OrderRequestValidator());
}
}
}

Custom Validators

Let us say we want to build extensible custom validator, in our case validating Zip code to be all digits. There is not out of the box validator like validating Email etc, so we must implement our own custom validator extending IRuleBuilder as shown below.

using FluentValidation;

namespace BusinessLogic.Validators
{
public static class FluentValidationExtensions
{
public static IRuleBuilderOptions<T, string> ZipCode<T>(this IRuleBuilder<T, string> ruleBuilder)
{
//"^[0-9]{5}(?:-[0-9]{4})?$"
return ruleBuilder
.NotEmpty()
.Matches(@"\d{5}$");
}
}
}

Wiring Everything

For using all these validators in our web api project we need to register them and use them as shown below.

using BusinessLogic.Validators;
using FluentValidation;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Add each and every validator one after another
//builder.Services.AddScoped<IValidator<CreateCustomerRequestValidator>, CreateCustomerRequestValidator>();
//Register all the validators in one go
builder.Services.AddValidatorsFromAssemblyContaining<CreateCustomerRequestValidator>();


builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();
using BusinessLogic.Models;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;

namespace OrderApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class OrderController : ControllerBase
{
private IValidator<CreateCustomerRequest> _validator;


public OrderController(IValidator<CreateCustomerRequest> validator)
{
_validator = validator;
}

[HttpPost]
public async Task<ActionResult> PostCustomer([FromBody] CreateCustomerRequest createCustomerRequest)
{
ValidationResult result = await _validator.ValidateAsync(createCustomerRequest);

if (!result.IsValid)
{
return BadRequest(result);
}

//rest of the logic
return Created();
}
}
}

Once everything is done we can test the validators using swagger and we can see below how we get validation errors.

Custom Validator output

Unit tests

We can easily write unit tests for validators as shown below:

using BusinessLogic.Validators;
using FluentValidation.TestHelper;

namespace BusinessLogic.Tests
{
[TestFixture]
public class AddressRequestValidatorTests
{
private readonly AddressRequestValidator _validator = new AddressRequestValidator();

[Test]
public void GivenValidLine1_Should_Not_Have_Error()
{
var result = _validator.TestValidate(new Models.AddressRequest()
{
Line1 = "123 Main Street"
});

result.ShouldNotHaveValidationErrorFor(add => add.Line1);
}

[Test]
public void GivenInvalidZipCode_Should_Have_Error()
{
var result = _validator.TestValidate(new Models.AddressRequest()
{
PostalCode = "ABCDE"
});

result.ShouldHaveValidationErrorFor(add => add.PostalCode);
}

[Test]
public void GivenvalidZipCode_Should_Not_Have_Error()
{
var result = _validator.TestValidate(new Models.AddressRequest()
{
PostalCode = "78613"
});

result.ShouldNotHaveValidationErrorFor(add => add.PostalCode);
}
}
}

With this we conclude this blog and below is the link for source code repository.

🙏Thanks for taking the time to read the article. If you found it helpful and would like to show support, please consider:

  1. 👏👏👏👏👏👏Clap for the story and bookmark for future reference
  2. Follow me on Chaitanya (Chey) Penmetsa for more content
  3. Stay connected on LinkedIn.

Wishing you a happy learning journey 📈, and I look forward to sharing new articles with you soon.

--

--

Chaitanya (Chey) Penmetsa
CodeNx
Editor for

👨🏽‍💻Experienced and passionate software enterprise architect helping solve real-life business problems with innovative, futuristic, and economical solutions.