Modèle CQRS: théorie et pratique dans ASP.Net Core 5

La vitesse de développement et la productivité des programmeurs peuvent varier en fonction de leur niveau et des technologies utilisées dans les projets. Il n'y a pas de norme pour la conception de logiciels





arts et GOST, vous seul choisissez comment vous allez développer votre programme. L'un des meilleurs moyens d'améliorer l'efficacité de votre travail consiste à appliquer le modèle de conception 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





Créons un pipeline pour la 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();
       }
   }
}
      
      



Et enregistrez-le avec DI, ajoutez l'initialisation de tous les validateurs pour FluentValidation.





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



Maintenant écrivons notre validateur.





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



Grâce aux capacités de C #, FluentValidation et MediatR, nous avons pu encapsuler notre logique d'équipe / requête dans une seule classe.





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



Cela a grandement simplifié le travail avec l'API et résolu tous les principaux problèmes.





Le résultat est un beau code encapsulé qui est compréhensible par tous les employés. Ainsi, nous pouvons rapidement introduire une personne dans le processus de développement, réduire les coûts et le temps de sa mise en œuvre. 





Les résultats actuels peuvent être consultés sur GitHub .








All Articles