Implementing CQRS Validation using the MediatR Pipeline and FluentValidation



Validation is a crucial aspect that must be addressed in your application. It is essential to ensure that each request is valid before proceeding with the processing.

Another critical consideration is the approach to different types of validation. Input validation and business validation should be treated differently and require specific solutions.

In this blog post, I will present an elegant solution for validation using MediatR and FluentValidation.

Even if you are not using CQRS with MediatR, don't worry. The concepts and techniques I explain regarding validation can be easily adapted to other paradigms.

Here's what I will cover in this week's newsletter:

1. Standard approach of validation 
2. Input vs. business validation
3. Separating validation logic
4. Generic ValidationBehavior

Now, let's dive in.

The Standard Approach of Command Validation :

Typically, validation is implemented just before processing the command. However, this tightly couples the validation with the command handler, which can become problematic.

As the complexity of the validation increases, maintaining this approach becomes challenging. Any changes to the validation logic directly impact the handler, leading to potential code bloating and loss of control.

Additionally, differentiating between input and business validation becomes more difficult.

For example, consider a CreateEmployeeCommandHandler that checks if the email has been registered  with any other employee:


 public class CreateEmployeeCommandHandler : IRequestHandler<CreateEmployeeCommand>
 {
     private readonly IEmployeeRepository _employeeRepository;

     public CreateEmployeeCommandHandler(IEmployeeRepository employeeRepository)
     {
         _employeeRepository = employeeRepository;
     }

     public async Task Handle([NotNull] CreateEmployeeCommand request, CancellationToken cancellationToken)
     {
         if (await ValidEmail(request.Email, cancellationToken))
         {
             throw new ArgumentException("\"This email has already been used.");
         }
         var employee = Employee.Create(
             request.FirstName,
             request.LastName,
             request.Email,
             request.ProfilePictureUrl,
             request.DesignationId,
             request.DepartmentId
         );

         _ = await _employeeRepository.InsertAsync(employee, cancellationToken);

         await _employeeRepository.UnitOfWork.CommitChangesAsync(cancellationToken);

         return;
     }

     private async Task<bool> ValidEmail(string email, CancellationToken cancellationToken)
     {
         var emailExists = await _employeeRepository.GetAnyAsync(e => e.Email == email, cancellationToken);
         return !emailExists;
     }
 }

Separating Command Validation and Handling

What if we could separate the command validation from the handling process?

Input Validation and Business Validation:

Input validation and business validation are two distinct types of validation.

Input validation ensures that the command is processable and involves simple validations, such as checking for null values or empty strings.

Business validation, on the other hand, validates the command against business rules, including preconditions specific to the system's state before processing the command.

To compare them, input validation is usually cheaper and can be executed in memory. In contrast, business validation often involves reading system state and is slower.

It is crucial to perform input validation at the entry point of the use case before handling the request. Following this rule ensures that only valid commands reach the handler.

Use FluentValidation for input Validation :

FluentValidation is a powerful validation library for .NET that uses a fluent interface and lambda expressions to define strongly-typed validation rules.

To implement a validator with FluentValidation, you need to create a class that inherits from the AbstractValidator<T> base class. Then, using the RuleFor method in the constructor, you can add validation rules.

    public class CreateEmployeeCommand : IRequest
    {
        public virtual string FirstName { get; set; }
        public virtual string LastName { get; set; }
        public virtual string Email { get; set; }
        public virtual EmploymentStatus EmploymentStatus { get; set; }
        public virtual Guid DesignationId { get; set; }
        public virtual Guid DepartmentId { get; set; }
        public virtual string ProfilePictureUrl { get; set; }
    }

To automatically register all validators from an assembly, you can use the AddValidatorsFromAssembly method.

services.AddValidatorsFromAssembly(ApplicationAssembly.Assembly);

Executing Validation From the Use Case:

To run the CreateEmployeeCommandValidator, you can use the IValidator<T> service and inject it into the command handler.

The validator provides methods like Validate, ValidateAsync, or ValidateAndThrow. The Validate method returns a ValidationResult object containing the validation result.

public class CreateEmployeeCommandHandler : IRequestHandler<CreateEmployeeCommand>
{
    private readonly IEmployeeRepository _employeeRepository;
    private readonly IValidator<CreateEmployeeCommand> _validator;

    public CreateEmployeeCommandHandler(IEmployeeRepository employeeRepository)
    {
        _employeeRepository = employeeRepository;
    }

    public async Task Handle([NotNull] CreateEmployeeCommand request, CancellationToken cancellationToken)
    {
        _validator.ValidateAndThrow(request);

        var employee = Employee.Create(
            request.FirstName,
            request.LastName,
            request.Email,
            request.ProfilePictureUrl,
            request.DesignationId,
            request.DepartmentId
        );

        _ = await _employeeRepository.InsertAsync(employee, cancellationToken);

        await _employeeRepository.UnitOfWork.CommitChangesAsync(cancellationToken);

        return;
    }
}

This approach requires each command handler to have an explicit dependency on IValidator.

But what if we could implement this cross-cutting concern in a more generic way?

Validation Pipeline - MediatR:

The MediatR Validation Pipeline is a fully implemented ValidationBehavior that combines FluentValidation and MediatR's IPipelineBehavior.

Functioning as middleware within the request pipeline, the ValidationBehavior executes validation tasks. In case the validation process encounters an error, it raises a custom ValidationException containing a set of ValidationError objects.


 public class CreateEmployeeCommandValidator : AbstractValidator<CreateEmployeeCommand>
 {
     private readonly IEmployeeRepository _employeeRepository;
     public CreateEmployeeCommandValidator(IEmployeeRepository employeeRepository)
     {
         _employeeRepository = employeeRepository;

         RuleFor(a => a.FirstName)
             .NotEmpty()
             .WithMessage("{PropertyName} Cannot be empty")
             .MaximumLength(EmployeeConstants.NameMaxLength)
             .WithMessage("{PropertyName} Cannot contain more than {MaxLength} characters");

         RuleFor(a => a.LastName)
             .NotEmpty()
             .WithMessage("{PropertyName} Cannot be empty")
             .MaximumLength(EmployeeConstants.NameMaxLength)
             .WithMessage("{PropertyName} Cannot contain more than {MaxLength} characters");

         RuleFor(a => a.Email)
             .NotEmpty()
             .WithMessage("{PropertyName} Cannot be empty")
             .EmailAddress()
             .WithMessage("{PropertyName} Should be valid Email")
             .MaximumLength(EmployeeConstants.EmailMaxLength)
             .WithMessage("{PropertyName} Cannot be longer than {MaxLength} characters")
             .Must(email => !email.Contains("+"))
             .WithMessage("{PropertyName} cannot contain a (+) emails")
             .MustAsync(ValidEmail).WithMessage("This email has already been used.");
     }

     private async Task<bool> ValidEmail(string email, CancellationToken cancellationToken)
     {
         var emailExists = await _employeeRepository.GetAnyAsync(e=>e.Email == email, cancellationToken); 
         return !emailExists;
     }
 }


Make sure to add the ValidationBehavior to MediatR by calling AddOpenBehavior,

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssemblyContaining<ApplicationAssembly>();
    cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
};

Dealing with Validation Exceptions:

To handle the custom ValidationException, you can create a custom middleware called ExceptionHandlerBuilderExtensions, which converts the exception to a ProblemDetails response and includes the validation errors.

 public static class ExceptionHandlerBuilderExtensions
 {

     public static void UseApiExceptionHandler(this IApplicationBuilder app)
     {
         app.UseExceptionHandler(appError =>
         {
             appError.Run(async context =>
             {
                 var contextFeature = context.Features.Get<IExceptionHandlerFeature>();

                 if (contextFeature != null)
                 {
                     var (code, error) = GetError(contextFeature.Error);

                     context.Response.StatusCode = code;
                     context.Response.ContentType = "application/json";

                     //Error log by Micrososft

                     var content = JsonSerializer.Serialize(error, new JsonSerializerOptions()
                     {
                         PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                         ReferenceHandler = ReferenceHandler.IgnoreCycles
                     });
                     await context.Response.WriteAsync(content);

                 }
             });
         });
     }

     private static (int code, ErrorResponse error) GetError(Exception exception)
     {
         var response = new ErrorResponse
         {
             Error = new ErrorDetail()
         };

         int code;
         if (exception is ValidationException ve)
         {
             code = (int)HttpStatusCode.BadRequest;
             response.Error.Code = "BadArgument";
             response.Error.Message = "One or more validation errors occurred.";
             response.Error.Details = new List<ErrorDetail>
             {
                 new ErrorDetail()
                 {
                     Target = ve.Parmaeter,
                     Message = ve.Message,
                 }
             };

         }
         else if (exception is EntityNotFoundException ne)
         {
             code = (int)HttpStatusCode.NotFound;

             response.Error.Code = "NotFound";
             response.Error.Message = "Entity not found";
             response.Error.Details = new ErrorDetail[]
             {
                 new ErrorDetail()
                 {
                     Code = "NotFound",
                     Target = ne.EntityType.Name,
                     Message = ne.Message,
                 }
             };
         }
         else if (exception is EntityStateConflictException ce)
         {
             code = (int)HttpStatusCode.Conflict;

             response.Error.Code = "Conflict";
             response.Error.Message = "Entity is in conflict state due to arguements";
             response.Error.Details = new ErrorDetail[]
             {
                 new ErrorDetail()
                 {
                     Code = "Conflict",
                     Target = ce.EntityType.Name,
                     Message = ce.Message,
                 }
             };
         }
         else if (exception is NotAllowedOperationException nae)
         {
             code = (int)HttpStatusCode.Forbidden;

             response.Error.Code = "AccessDenied";
             response.Error.Message = "Not allowed to execute operation due to insufficient access";
             response.Error.Details = new ErrorDetail[]
             {
                 new ErrorDetail()
                 {
                     Code = "AccessDenied",
                     Message = nae.Message,
                 }
             };
         }
         else
         {
             code = (int)HttpStatusCode.InternalServerError;
             response.Error.Code = "InternalError";
             response.Error.Message = exception.Message;
         }



         return (code, response);
     }
 }

Make sure to include this middleware in the request pipeline using UseMiddleware.

Summary :

The implementation of ValidationBehavior using MediatR and FluentValidation presented here has been proven effective in real projects. It provides a clean and flexible approach to validation.

If you are not using MediatR, you can still apply the concepts by implementing middleware and incorporating the validation logic into it. ASP.NET Core offers multiple ways to create middleware.

I hope you find this information valuable and can apply it to your application.

Comments

Popular posts from this blog

The Significance of Wireframing and Modeling of API

Handling Distributed Transactions In Microservices