CQRS pattern: theory and practice in ASP.Net Core 5

The development speed and productivity of programmers may vary depending on their level and the technologies used in projects. There is no standard for software design





arts and GOSTs, only you choose how you will develop your program. One of the best ways to improve your work efficiency is to apply the CQRS design pattern. 





CQRS: Regular, Progressive Deluxe. โ€” Regular CQRS, DD Planet - ยซ.ยป. Progressive Deluxe โ€” .





: CQRS - , . 





Onion

, CQRS, , .





ยซยป :





  1. โ€” .





  2. -, .





  3. โ€” .





  4. : UI, .





, , . 





ยซ.ยป. -, . , . , โ€” . , , .





, โ€” , . CQRS. 





CQRS

 

CQRS (Command Query Responsibility Segregation)โ€” , :





  • โ€” ;





  • โ€” , . 





. , (tiers), .





, , . -, ยซ.ยป, :









  • ;





  • ;





  • ;





  • .





CQRS , ( , ), , , , , /, . 





, CQRS , .





ASP.NET Core 5.0, .





ASP.NET Core 5.0, :





  • MediatRโ€” , Mediator, / .





  • FluentValidationโ€” .NET, Fluent- - .





REST API CQRS

REST API:





  • get โ€” ; 





  • post, put, delete โ€” .





MediatR:

, :





dotnet add package MediatR.Extensions.Microsoft.DependencyInjection





ConfigureServices Startup:





namespace CQRS.Sample
{
   public class Startup
   {
       ...
       public void ConfigureServices(IServiceCollection services)
       {
           ...
           services.AddMediatR(Assembly.GetExecutingAssembly());
           services.AddControllers();
           ...
       }
   }
}

      
      



, . , MediatR IRequest<TResponse>, .





namespace CQRS.Sample.Features
{
   public class AddProductCommand : IRequest<Product>
   {
       /// <summary>
       ///      
       /// </summary>
       public string Alias { get; set; }
 
       /// <summary>
       ///      
       /// </summary>
       public string Name { get; set; }
 
       /// <summary>
       ///      
       /// </summary>
       public ProductType Type { get; set; }
   }
}
      
      



IRequestHandler<TCommand, TResponse>. 





, , -, โ€” .





namespace CQRS.Sample.Features
{
   public class AddProductCommand : IRequest<Product>
   {
       /// <summary>
       ///      
       /// </summary>
       public string Alias { get; set; }
 
       /// <summary>
       ///      
       /// </summary>
       public string Name { get; set; }
 
       /// <summary>
       ///      
       /// </summary>
       public ProductType Type { get; set; }
 
       public class AddProductCommandHandler : IRequestHandler<AddProductCommand, Product>
       {
           private readonly IProductsRepository _productsRepository;
 
           public AddProductCommandHandler(IProductsRepository productsRepository)
           {
               _productsRepository = productsRepository ?? throw new ArgumentNullException(nameof(productsRepository));
           }
 
           public async Task<Product> Handle(AddProductCommand command, CancellationToken cancellationToken)
           {
               Product product = new Product();
               product.Alias = command.Alias;
               product.Name = command.Name;
               product.Type = command.Type;
 
               await _productsRepository.Add(product);
               return product;
           }
       }
   }
}
      
      



, Action , IMediator . , ASP.Net Core . MediatR .





namespace CQRS.Sample.Controllers
{
   [Route("api/v{version:apiVersion}/[controller]")]
   [ApiController]
   public class ProductsController : ControllerBase
   {
       private readonly ILogger<ProductsController> _logger;
       private readonly IMediator _mediator;
 
       public ProductsController(IMediator mediator)
       {
           _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
       }
       
       ...
 
       /// <summary>
       ///      
       /// </summary>
       /// <param name="client"></param>
       /// <param name="apiVersion"></param>
       /// <param name="token"></param>
       /// <returns></returns>
       [HttpPost]
       [ProducesResponseType(typeof(Product), StatusCodes.Status201Created)]
       [ProducesDefaultResponseType]
       public async Task<IActionResult> Post([FromBody] AddProductCommand client, ApiVersion apiVersion,
           CancellationToken token)
       {
           Product entity = await _mediator.Send(client, token);
           return CreatedAtAction(nameof(Get), new {id = entity.Id, version = apiVersion.ToString()}, entity);
       }
   }
}
      
      



MediatR /, , , Middlewares ASP.Net Core . , .





FluentValidation.





FluentValidation :





dotnet add package FluentValidation.AspNetCore





Let's create a Pipeline for validation:





namespace CQRS.Sample.Behaviours
{
   public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
       where TRequest : IRequest<TResponse>
   {
       private readonly ILogger<ValidationBehaviour<TRequest, TResponse>> _logger;
       private readonly IEnumerable<IValidator<TRequest>> _validators;
 
       public ValidationBehaviour(IEnumerable<IValidator<TRequest>> validators,
           ILogger<ValidationBehaviour<TRequest, TResponse>> logger)
       {
           _validators = validators;
           _logger = logger;
       }
 
       public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
           RequestHandlerDelegate<TResponse> next)
       {
           if (_validators.Any())
           {
               string typeName = request.GetGenericTypeName();
 
               _logger.LogInformation("----- Validating command {CommandType}", typeName);
 
 
               ValidationContext<TRequest> context = new ValidationContext<TRequest>(request);
               ValidationResult[] validationResults =
                   await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
               List<ValidationFailure> failures = validationResults.SelectMany(result => result.Errors)
                   .Where(error => error != null).ToList();
               if (failures.Any())
               {
                   _logger.LogWarning(
                       "Validation errors - {CommandType} - Command: {@Command} - Errors: {@ValidationErrors}",
                       typeName, request, failures);
 
                   throw new CQRSSampleDomainException(
                       $"Command Validation Errors for type {typeof(TRequest).Name}",
                       new ValidationException("Validation exception", failures));
               }
           }
 
           return await next();
       }
   }
}
      
      



And register it with DI, add initialization of all validators for FluentValidation.





namespace CQRS.Sample
{
   public class Startup
   {
       ...
       public void ConfigureServices(IServiceCollection services)
       {
           ...
           services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
           services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
           ...
       }
   }
}
      
      



Now let's write our validator.





public class AddProductCommandValidator : AbstractValidator<AddProductCommand>
{
   public AddProductCommandValidator()
   {
       RuleFor(c => c.Name).NotEmpty();
       RuleFor(c => c.Alias).NotEmpty();
   }
}
      
      



Thanks to the capabilities of C #, FluentValidation and MediatR, we were able to encapsulate our team / request logic within a single class.





namespace CQRS.Sample.Features
{
   public class AddProductCommand : IRequest<Product>
   {
       /// <summary>
       ///      
       /// </summary>
       public string Alias { get; set; }
 
       /// <summary>
       ///      
       /// </summary>
       public string Name { get; set; }
 
       /// <summary>
       ///      
       /// </summary>
       public ProductType Type { get; set; }
 
       public class AddProductCommandHandler : IRequestHandler<AddProductCommand, Product>
       {
           private readonly IProductsRepository _productsRepository;
 
           public AddProductCommandHandler(IProductsRepository productsRepository)
           {
               _productsRepository = productsRepository ?? throw new ArgumentNullException(nameof(productsRepository));
           }
 
           public async Task<Product> Handle(AddProductCommand command, CancellationToken cancellationToken)
           {
               Product product = new Product();
               product.Alias = command.Alias;
               product.Name = command.Name;
               product.Type = command.Type;
 
               await _productsRepository.Add(product);
               return product;
           }
       }
 
       public class AddProductCommandValidator : AbstractValidator<AddProductCommand>
       {
           public AddProductCommandValidator()
           {
               RuleFor(c => c.Name).NotEmpty();
               RuleFor(c => c.Alias).NotEmpty();
           }
       }
   }
}
      
      



This greatly simplified the work with the API and solved all the main problems.





The result is a beautiful encapsulated code that is understandable to all employees. So, we can quickly introduce a person into the development process, reduce costs and time for its implementation. 





The current results can be viewed on GitHub .








All Articles