Mastering Validation with MediatR and FluentValidation in Clean Architecture
Table of Contents
- Introduction
- What are Mediator Pipeline Behaviors?
- Creating a Validation Behavior
- Setting Up Fluent Validation
- Creating a Register Command Validator
- Implementing the Validation Behavior
- Registering the Behavior in the Dependency Injection
- Testing the Validation Behavior
- Handling Validation Errors
- Exploring the Mediator Package Implementation
Introduction
In this article, we will explore mediator pipeline behaviors and how they can be used to validate mediator requests. We will specifically focus on creating a behavior that utilizes the Fluent Validation library for request validation. If You are unfamiliar with Fluent Validation, make sure to check out our video on the topic before proceeding. It will help you follow along more effectively.
Disclaimer
Before we dive into the details, I want to provide a quick disclaimer. Although I am a Microsoft employee and we will be discussing Microsoft technologies, I am sharing my personal opinions and not speaking on behalf of Microsoft.
What are Mediator Pipeline Behaviors?
Mediator pipeline behaviors are a powerful feature of the Mediator package. They allow us to Create a pipeline of behaviors that our requests can go through before reaching their corresponding handlers. Similar to how requests in a traditional web application go through a pipeline of Middleware before reaching the endpoints, the Mediator package allows us to define a pipeline of behaviors for requests sent using the mediator pattern.
In our application, we currently have the register and login endpoints, which take incoming requests and convert them to the corresponding commands or queries. We then use Mediator to send these commands or queries to their respective handlers. The handlers are located in the application layer of our solution.
Creating a Validation Behavior
To demonstrate how to create a mediation pipeline behavior for validation, we will start by implementing a behavior specifically for the register command. This will allow us to validate the register request before it reaches the handler.
In the application layer, create a new folder named "Behaviors" inside the "Common" folder. Inside the "Behaviors" folder, create a new class called "ValidateRegisterCommandBehavior".
The class should implement the IPipelineBehavior<TRequest, TResponse>
interface, which requires two Type parameters. The first parameter represents the mediator request type we want to surround with this behavior (in this case, the register command). The Second parameter represents the response type.
Within the class, implement the Handle
method as follows:
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var validationResult = await next();
// Additional logic and manipulation of the result can be performed here
return validationResult;
}
This method receives the request, a cancellation token, and the RequestHandlerDelegate<TResponse>
delegate. The delegate represents the next step in the pipeline, which is typically the handler responsible for processing the request.
You can perform any pre-processing or post-processing logic on the request or the result before and after invoking the handler. For example, you may want to manipulate the request or result, log certain information, or perform additional checks.
To orchestrate the pipeline behavior, you need to register it in the dependency injection container. Go to the dependency injection file and add the following code inside the ConfigureServices
method:
services.AddScoped(typeof(IPipelineBehavior<RegisterCommand, ValidationResult>), typeof(ValidateRegisterCommandBehavior));
This code registers the ValidateRegisterCommandBehavior
as a scoped service and wires it up to the specific mediator request and response types.
Now, every time the mediator sends a register command, the ValidateRegisterCommandBehavior
behavior will be invoked, validating the request before it reaches the handler.
Setting Up Fluent Validation
Before we proceed further, we need to add the Fluent Validation library to our project. This library provides a convenient way to define validation rules for our objects.
To add Fluent Validation, navigate to the application layer project and run the following command:
dotnet add package FluentValidation
Once the package is installed, we can start utilizing it to define our validator classes.
Creating a Register Command Validator
To validate the register command, we will create a validator class specifically for this command. Since We Are following the clean architecture approach, we will create the validator inside the corresponding feature folder for the register command.
Create a new class called RegisterCommandValidator
and make it implement the AbstractValidator<RegisterCommand>
class from the Fluent Validation library.
Within the class, define the rules for validating the properties of the RegisterCommand
object. For example, you may want to ensure that all the properties are not empty. Here is an example of how the RegisterCommandValidator
class might look:
public class RegisterCommandValidator : AbstractValidator<RegisterCommand>
{
public RegisterCommandValidator()
{
RuleFor(x => x.FirstName).NotEmpty();
RuleFor(x => x.LastName).NotEmpty();
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Password).NotEmpty().MinimumLength(8);
}
}
In this example, we use the RuleFor
method to define the rules for each property. For instance, we want to ensure that the FirstName
, LastName
, Email
, and Password
properties are not empty. Additionally, we specify that the Email
property must be a valid email address and that the Password
property must be at least 8 characters long.
To register the RegisterCommandValidator
in the dependency injection container, go back to the dependency injection file and add the following code inside the ConfigureServices
method:
services.AddScoped<IValidator<RegisterCommand>, RegisterCommandValidator>();
This code registers the RegisterCommandValidator
as a scoped service. Now, whenever a validation is triggered for a RegisterCommand
object, the RegisterCommandValidator
will be used to perform the validation.
Implementing the Validation Behavior
Now that we have the registration and validation set up, we can implement the actual validation behavior in the ValidateRegisterCommandBehavior
class.
Inject the IValidator<RegisterCommand>
validator into the ValidateRegisterCommandBehavior
class by adding it as a constructor parameter:
private readonly IValidator<RegisterCommand> _validator;
public ValidateRegisterCommandBehavior(IValidator<RegisterCommand> validator)
{
_validator = validator;
}
Within the Handle
method, use the injected validator to validate the register command request:
var validationResults = await _validator.ValidateAsync(request);
The ValidateAsync
method returns a ValidationResult
object which contains the result of the validation. Next, check if the ValidationResult
is valid. If it is, simply invoke the next step in the pipeline by calling the next()
delegate:
if (validationResult.IsValid)
{
return await next();
}
Otherwise, if the ValidationResult
is invalid, convert the validation failures to a list of errors using the Arrow library:
var errors = validationResults.Errors
.Select(failure => new ValidationError(failure.PropertyName, failure.ErrorMessage))
.ToList();
The ValidationError
class is a custom class that represents an error in our application. Each error has a property name and an error message.
Finally, return the list of errors as the response instead of invoking the next step in the pipeline:
return errors;
This way, if the request fails validation, the errors will be returned to the client instead of invoking the handler.
Registering the Behavior in the Dependency Injection
To ensure that the validation behavior is executed before the corresponding handler, we need to register it in the dependency injection container.
Open the dependency injection file and add the following code inside the ConfigureServices
method:
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidateRegisterCommandBehavior<,>));
This code registers the ValidateRegisterCommandBehavior
as a transient service and wires it up to the generic IPipelineBehavior
interface with the appropriate type parameters.
Now, every time the mediator sends a request of type RegisterCommand
, the ValidateRegisterCommandBehavior
will be invoked before the corresponding handler.
Testing the Validation Behavior
To test whether the validation behavior works as expected, we can create some sample requests and observe the behavior.
Run the application and navigate to the register endpoint. Send a request with valid details and observe that the request is successfully processed by the handler.
Next, send a request with invalid details (e.g., missing required fields or an invalid email format) and observe that the request fails validation and the corresponding error response is returned instead of invoking the handler.
This confirms that the validation behavior is working correctly and validating requests before they reach the handler.
Handling Validation Errors
Currently, when a validation error occurs, the error response returned to the client is not formatted as a proper validation problem response. To improve this, we can update our error handling strategy to provide a more Meaningful response.
Update the Problem
method in the ApiController
class to handle validation errors specifically. In the case of validation errors, we should return a validation problem response instead of a regular error response.
private IActionResult Problem(ValidationResult validationResult)
{
if (validationResult.Errors.All(e => e.Type == "Validation"))
{
var modelStateDictionary = validationResult.Errors
.ToDictionary(e => e.PropertyName, e => e.ErrorMessage);
return ValidationProblem(modelStateDictionary);
}
// Regular error handling logic for other types of errors
return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred");
}
Within the Problem
method, we check if all the errors in the ValidationResult
are of type "Validation". If they are, we create a ModelStateDictionary
from the errors and return a ValidationProblem
response. This response will include the status code 400 and detailed error messages for each invalid property.
If there are other types of errors, you can handle them separately Based on your application logic.
By implementing this change, our API controller will now return a proper validation problem response when validation errors occur.
Exploring the Mediator Package Implementation
Before we conclude, let's briefly explore the implementation of mediator pipeline behaviors in the Mediator package. This will give you a deeper understanding of how the behaviors work behind the scenes.
Open the Mediator package in a code editor and navigate to the Mediator.cs
file. Look for the Send
method, which is the entry point for executing mediator requests.
Within the Send
method, you will see the logic that orchestrates the pipeline behaviors. It retrieves all the behaviors registered for the specific request and response types and creates a pipeline chain using the RequestHandlerDelegate<TResponse>
delegate.
The RequestHandlerDelegate<TResponse>
delegate represents the next step in the pipeline, which is typically the handler responsible for processing the request. Each behavior in the pipeline invokes the next behavior or the handler in turn.
The implementation uses the Aggregate
method to chain the behaviors together. For each behavior, a new delegate is created that first invokes the behavior and then invokes the next delegate. This recursive invocation pattern allows the behaviors to be invoked in the intended order.
If you ever need to customize the behavior of the mediator or dive deeper into how it works, the Mediator package's implementation is a great resource to explore.
Conclusion
In this article, we have learned how to create and use mediator pipeline behaviors for request validation. We explored the implementation of a validation behavior using Fluent Validation and demonstrated how to register and test it.
By using mediator pipeline behaviors, we can ensure that requests are validated before reaching their handlers, improving the overall integrity and reliability of our application.