Validate your Records, Settings, DTOs and whatever else you can think with Fluent Validation.
Ahhh validation of properties and values, I think nobody like to do this work, but it is super important when transferring, reading, deserializing data.
In this post I will show you that using Fluent Validation it is great for this purpose.
But first let us begin with some of the concepts.
Records

If you have never used records, you should start using them. They provide easy declaration, cloning, comparers, formatting, etc.
The library that we will be using is Fluent Validation
Fluent Validation
Awesome library that allows us to decouple the validation from the code.
That out of our way, we will be using this library on a regular web API; and yes, I will be providing the sample code which you can clone from...
The solution structure stands as

Obviously, this is a demo, and you can separate the classes in different projects, but the main objective is to show ways on using the fluent validation.
Let's create the main character, the "Product" class. This can record will serve as DTO and an Entity.
In the real world, it would be 2 different classes, DTO and the other an entity framework class.
/// <summary>
/// Product
/// </summary>
/// <seealso cref="System.IEquatable<DarthSeldon.API.FluentValidation.Demo.Records.Product>" />
public record Product
{
#region Properties
/// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>
/// Identifier.
/// </value>
public int Id { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>
/// Name.
/// </value>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the description.
/// </summary>
/// <value>
/// Description.
/// </value>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the price.
/// </summary>
/// <value>
/// Price.
/// </value>
public decimal Price { get; set; }
#endregion Properties
}
Nothing out of the ordinary, just a couple properties, note that I prefer using Plain Old C# Objects (POCOs). I notice that this tends to be more uncoupled in the long run.
Next the validator
/// <summary>
/// Product Validator
/// </summary>
/// <seealso cref="FluentValidation.AbstractValidator<DarthSeldon.API.FluentValidation.Demo.Records.Product>" />
public class ProductValidator : AbstractValidator<Product>
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ProductValidator"/> class.
/// </summary>
public ProductValidator()
{
RuleFor(p => p.Name).NotEmpty();
RuleFor(p => p.Description).NotEmpty();
RuleFor(p => p.Price).NotNull().PrecisionScale(4, 2, false).GreaterThan(0);
}
#endregion Constructors
}
We will be using the constructor as the validator (Thanks Dependency Injection).
Simple enough, we are validating the properties that enforces some of the properties not to be an empty string and one property to check that is not null and with a precision of 4 numeric elements with 2 decimal points and greater that zero. You can create your own rules or use the rules that are already built in the library.
I won't go over the product logic class, as it is just a mockup. But suffice to say that this class will handle the business logic (using entity framework) and the logic for the CRUD operations will happen here.
Now for dependency injection of the logic and validator.
using DarthSeldon.API.FluentValidation.Demo.Business;
using DarthSeldon.API.FluentValidation.Demo.Records;
using DarthSeldon.API.FluentValidation.Demo.Validators;
using FluentValidation;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//add validators
builder.Services.AddScoped<IValidator<Product>, ProductValidator>();
//add business
builder.Services.AddScoped<ProductLogic>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Note that we are using also swagger UI. Will explain more in detail Open API standards and how it works amazing with API Managers in another post.
The validator and logic are injected in scoped service, it is recommended to used scoped as the APIs are stateless.
And finally, we will be using the validator
/// <summary>
/// Products Controller
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.ControllerBase" />
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
#region Members
/// <summary>
/// The logger
/// </summary>
private readonly ILogger<ProductsController> _logger;
/// <summary>
/// The product logic
/// </summary>
private readonly ProductLogic _productLogic;
/// <summary>
/// The validator
/// </summary>
private readonly IValidator<Product> _validator;
#endregion Members
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ProductsController"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="productLogic">The product logic.</param>
/// <param name="validator">The validator.</param>
public ProductsController(ILogger<ProductsController> logger, ProductLogic productLogic, IValidator<Product> validator)
{
_logger = logger;
_productLogic = productLogic;
_validator = validator;
}
#endregion Constructors
#region API
/// <summary>
/// Gets Products
/// </summary>
/// <returns>List of products</returns>
[HttpGet(Name = "GetProducts")]
[Produces("application/json")]
[ProducesResponseType(typeof(IEnumerable<Product>), 200)]
public IActionResult Get()
{
return new JsonResult(_productLogic.ObtainProducts());
}
/// <summary>
/// Creates a product
/// </summary>
/// <param name="product">Product.</param>
/// <returns>Product</returns>
[HttpPost(Name = "PostProduct")]
[Consumes("application/json")]
[Produces("application/json")]
[ProducesResponseType(typeof(Product), 200)]
[ProducesResponseType(typeof(APIError), 400)]
public IActionResult Post([FromBody] Product product)
{
var validationResult = _validator.Validate(product);
if (validationResult is not null && !validationResult.IsValid)
return new JsonResult(new APIError
{
Entity = JsonSerializer.Serialize(product),
Errors = (from err in validationResult.Errors
select err.ErrorMessage).ToList()
})
{
StatusCode = StatusCodes.Status400BadRequest
};
return new JsonResult(_productLogic.CreateProduct(product));
}
#endregion API
}
Please remind me to create a post for ODATA. We will be using the same codebase, but we will enhance it.
If you are curious now, you can check the basics

For now, we will be creating just 2 endpoints, the get products and create product.
Create product will be using our validator.
var validationResult = _validator.Validate(product);
if (validationResult is not null && !validationResult.IsValid)
return new JsonResult(new APIError
{
Entity = JsonSerializer.Serialize(product),
Errors = (from err in validationResult.Errors
select err.ErrorMessage).ToList()
})
{
StatusCode = StatusCodes.Status400BadRequest
};
return new JsonResult(_productLogic.CreateProduct(product));
We will be using the injected validator and if there are any errors, we will be capturing those and send them back to the consumer in JSON format. Note that I am not sending the validation result back, as it will create a dependency to the consumer, so we will be using another record and obtain the errors and send back to the consumer the sent product so they can check what they send incorrectly.
Since we are using Swagger UI, we can test directly the endpoints.

And to test the error, just send incorrect data and we will see the results from the validation.

This is a simple example, but think about the possibilities
Personally, I use fluent validation for:
- Reading settings and reporting back incorrect setting values
- Validate the POCO in the consumer before it reaches the API
- DTO and Record validation
- Business Rules
- Localization of error messages
Happy coding!!!