When implementing web applications that manipulate information, there is always the need to validate data sent by the clients, ensuring business rules are properly implemented.
Previously I talked about the implementation of Command Query Responsibility Segregation (CQRS) and Event Sourcing (ES) using a mediator pattern and how to support transversal behavior via pipelines.
In this article I’m going to demonstrate how validation can be enforced into your commands and events before reaching the handlers, making sure that invalid data will be rejected by the API.
This approach not only reduces the amount of duplicated code, it also ensures validations won’t be forgotten, unless explicitly stated.
The project
To make it faster to implement the validation pipeline, I’m going to leverage this example on both of my previous articles, in which we implemented an endpoint to manage products and introduced both a logging and timeout pipelines.
The source code is available on GitHub.
The validations
To help us configure the rules we are going to use a very popular and one of my favorites libraries FluentValidation by creating classes implementing the interface IValidator<T>
.
These classes will be added to the dependency injection container and used by the pipeline to validate both the commands and events before reaching the handler.
Lets start by installing the NuGet FluentValidation
:
Create a Validations
folder at the project root level, with a subfolder Products
.
Inside the Products
folder create both a validator for CreateProductCommand
and CreatedProductEvent
by extending the class AbstractValidator<T>
(which itself implementes the interface IValidator<T>
) and configure some rules in the constructors:
1 | public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand> |
Feel free to create validators for all other commands and events, I’m just focusing on these for simplicity. You can also check the documentation for supported rules and detailed usage instructions.
Open the Startup.cs
file and register all classes implementing IValidator<T>
into the container:
1 | public class Startup |
The pipeline
Because for this example we are going to enforce validation only on commands and events, since they either mutate or represent the system state at a given point in time, the pipeline will be implemented as follows:
- Intercept any command or event;
- Resolve a required instance of
IValidator<TCommand>
orIValidator<TEvent>
from the container; - Invoke the method
ValidateAndThrowAsync
, failing with aValidationException
if something is invalid;
Inside the Pipelines
folder create a ValidationPipeline
class extending Pipeline
(because we only need some of the methods):
1 | public class ValidationPipeline : Pipeline |
Open the Startup.cs
file and add the ValidationPipeline
at least after the LoggingPipeline
ensuring, in case invalid data is submitted, we can still see it in the logs:
1 | services.AddMediator(o => |
If you now start the server and try to create a product with invalid data you will receive a ValidationException
.
Because returning an HTTP 500 due to invalid data would be confusing to the client, lets just finish this example by creating an ASP.NET Core middleware converting this exception into a more appropriate code, like HTTP 422.
Once again, open the Startup.cs
file and register the middleware immediately after the developer exception page, catching this exception and returning HTTP 422 with a more detailed JSON representation:
1 | public class Startup |
Submit invalid data again and a more detailed message should be returned:
1 | POST /products |
Speeding things up!
If just like me, you can see yourself using a pipeline for validation in most of your projects, there is already a pipeline available via NuGet that should be configurable to most use cases while also providing a simpler way to register the validators into the container.
Install SimpleSoft.Mediator.Microsoft.Extensions.ValidationPipeline
via NuGet:
Use the extension method AddPipelineForValidation
and enforce both command and event validations and use the extension method AddValidatorsFromAssemblyOf
to scan for all IValidator<T>
classes and register them into the container.
For the current project, the ConfigureServices
method would be similar to the following:
1 | public class Startup |
Conclusion
I hope this article gave you a good idea on how to use mediator pipelines to ensure that all your commands, events and even queries are initialized with proper data, either implementing your own pipeline or by using the existing ValidationPipeline NuGet.
I also made an article about transaction management with pipelines, you may also find it helpful.