Patrón CQRS: teoría y práctica en ASP.Net Core 5

La velocidad de desarrollo y la productividad de los programadores pueden variar según su nivel y las tecnologías utilizadas en los proyectos. No existe un estándar para el diseño de software





arts y GOSTs, solo tú eliges cómo desarrollarás tu programa. Una de las mejores formas de mejorar la eficiencia de su trabajo es aplicar el patrón de diseño CQRS. 





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





Creemos un Pipeline para la validación:





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();
       }
   }
}
      
      



Y regístrelo con DI, agregue la inicialización de todos los validadores para FluentValidation.





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



Ahora escribamos nuestro validador.





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



Gracias a las capacidades de C #, FluentValidation y MediatR, pudimos encapsular nuestra lógica de solicitud / equipo dentro de una sola clase.





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();
           }
       }
   }
}
      
      



Esto simplificó enormemente el trabajo con la API y resolvió todos los problemas principales.





El resultado es un hermoso código encapsulado que es comprensible para todos los empleados. Entonces, podemos introducir rápidamente a una persona en el proceso de desarrollo, reducir costos y tiempo para su implementación. 





Los resultados actuales se pueden ver en GitHub .








All Articles