use. NET 6 to develop TodoList application (11) -- using fluent validation and MediatR to implement interface request verification

Posted by Onloac on Wed, 05 Jan 2022 00:56:02 +0100

Series navigation and source code

demand

In the process of response request processing, we often need to verify the legitimacy of the request parameters. If the parameters are illegal, we will not continue to process the business logic. Of course, we can write the parameter verification logic of each interface to the corresponding Handle method, but a better way is to sort this part of code irrelevant to the actual business logic into a separate place for management with the help of the features provided by MediatR.

In order to achieve this requirement, we need to combine FluentValidation And features provided by MediatR.

target

The requested parameter verification logic is separated from the Handler of CQRS and processed in the Pipeline framework of MediatR.

Principles and ideas

MediatR not only provides a framework for implementing CQRS, but also provides an ipipelinebehavior < trequest, tresult > interface to implement a series of features that are not closely related to the actual business logic before CQRS response, such as request log, parameter verification, exception handling, authorization, performance monitoring and so on.

In this article, we will combine fluent validation and ipipelinebehavior < trequest, tresult > to implement the verification function of request parameters.

realization

Add MediatR parameter to verify Pipeline Behavior framework support

First, introduce fluent validation. XML into the Application project Dependencyinjectionextensionsnuget package. To abstract all validation exceptions, first create a ValidationException class:

  • ValidationException.cs
namespace TodoList.Application.Common.Exceptions;

public class ValidationException : Exception
{
    public ValidationException() : base("One or more validation failures have occurred.")
    {
    }

    public ValidationException(string failures)
        : base(failures)
    {
    }
}

The basic framework of parameter verification is created in Application/Common/Behaviors /

  • ValidationBehaviour.cs
using FluentValidation;
using FluentValidation.Results;
using MediatR;
using ValidationException = TodoList.Application.Common.Exceptions.ValidationException;

namespace TodoList.Application.Common.Behaviors;

public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    // Inject all custom Validators
    public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators) 
        => _validators = validators;

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);

            var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));

            var failures = validationResults
                .Where(r => r.Errors.Any())
                .SelectMany(r => r.Errors)
                .ToList();

            // If validator verification fails, an exception will be thrown. The exception here is our custom wrapper type
            if (failures.Any())
                throw new ValidationException(GetValidationErrorMessage(failures));
        }
        return await next();
    }

    // Format verification failure message
    private string GetValidationErrorMessage(IEnumerable<ValidationFailure> failures)
    {
        var failureDict = failures
            .GroupBy(e => e.PropertyName, e => e.ErrorMessage)
            .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray());

        return string.Join(";", failureDict.Select(kv => kv.Key + ": " + string.Join(' ', kv.Value.ToArray())));
    }
}

Dependency injection in DependencyInjection:

  • DependencyInjection.cs
// Omit other
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)

Add Validation Pipeline Behavior

Next, take adding TodoItem interface as an example, and create CreateTodoItemCommandValidator in Application/TodoItems/CreateTodoItem /

  • CreateTodoItemCommandValidator.cs
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using TodoList.Application.Common.Interfaces;
using TodoList.Domain.Entities;

namespace TodoList.Application.TodoItems.Commands.CreateTodoItem;

public class CreateTodoItemCommandValidator : AbstractValidator<CreateTodoItemCommand>
{
    private readonly IRepository<TodoItem> _repository;

    public CreateTodoItemCommandValidator(IRepository<TodoItem> repository)
    {
        _repository = repository;

        // We limit the maximum length to 10 to better verify this check
        // For more usage, please refer to the official documentation of fluent validation
        RuleFor(v => v.Title)
            .MaximumLength(10).WithMessage("TodoItem title must not exceed 10 characters.").WithSeverity(Severity.Warning)
            .NotEmpty().WithMessage("Title is required.").WithSeverity(Severity.Error)
            .MustAsync(BeUniqueTitle).WithMessage("The specified title already exists.").WithSeverity(Severity.Warning);
    }

    public async Task<bool> BeUniqueTitle(string title, CancellationToken cancellationToken)
    {
        return await _repository.GetAsQueryable().AllAsync(l => l.Title != title, cancellationToken);
    }
}

The parameter verification and addition method of other interfaces is similar to this, and the demonstration will not continue.

verification

Start the Api project. We create TodoItem with a request whose verification will fail:

  • request

  • response

In the previous test, a TodoItem was generated with the same request without verification, so two verifications in the message of verification failure were not met.

A little expansion

We mentioned earlier that the PipelineBehavior using MediatR can implement some logic before CQRS requests, including logging. Here, we also put the implementation method below. Here, we use the irequestpreprocessor < trequest > interface in Pipeline, because we only care about the information before request processing, If you care about the information returned after request processing, as before, you need to implement the ipipelinebehavior < trequest, tresponse > interface and return the response object in the Handle:

// Omit other
var response = await next();
//Response
_logger.LogInformation($"Handled {typeof(TResponse).Name}");

return response;

Create a LoggingBehavior:

using System.Reflection;
using MediatR.Pipeline;
using Microsoft.Extensions.Logging;

public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where TRequest : notnull
{
    private readonly ILogger<LoggingBehaviour<TRequest>> _logger;

    // In the constructor, we can also inject objects similar to ICurrentUser and IIdentity for log output
    public LoggingBehaviour(ILogger<LoggingBehaviour<TRequest>> logger)
    {
        _logger = logger;
    }

    public async Task Process(TRequest request, CancellationToken cancellationToken)
    {
        // You can log any information about the request here
        _logger.LogInformation($"Handling {typeof(TRequest).Name}");

        IList<PropertyInfo> props = new List<PropertyInfo>(request.GetType().GetProperties());
        foreach (var prop in props)
        {
            var propValue = prop.GetValue(request, null);
            _logger.LogInformation("{Property} : {@Value}", prop.Name, propValue);
        }
    }
}

If the ipipelinebehavior < trequest, tresponse > interface is implemented, it can be injected at last.

// Omit other
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>));

If the irequestpreprocessor < trequest > interface is implemented, no injection is required.

The effect is shown in the following figure:

You can see that the Command name and request parameter field values have been output in the log.

summary

In this paper, we implement the request parameter verification logic that does not invade business code through fluent validation and MediatR, which will be introduced in the next article NET development.

reference material

  1. FluentValidation
  2. How to use MediatR Pipeline Behaviours

Topics: webapi