Mastering Validation with MediatR and FluentValidation in Clean Architecture

Find AI Tools
No difficulty
No complicated process
Find ai tools

Mastering Validation with MediatR and FluentValidation in Clean Architecture

Table of Contents

  1. Introduction
  2. What are Mediator Pipeline Behaviors?
  3. Creating a Validation Behavior
  4. Setting Up Fluent Validation
  5. Creating a Register Command Validator
  6. Implementing the Validation Behavior
  7. Registering the Behavior in the Dependency Injection
  8. Testing the Validation Behavior
  9. Handling Validation Errors
  10. 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.

Are you spending too much time looking for ai tools?
App rating
4.9
AI Tools
100k+
Trusted Users
5000+
WHY YOU SHOULD CHOOSE TOOLIFY

TOOLIFY is the best ai tool source.

Browse More Content