[Совершенный код, .NET, API, C#, Микросервисы] Паттерн CQRS: теория и практика в рамках ASP.Net Core 5

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
24-Фев-2021 00:33


Скорость разработки и производительность программистов могут отличаться в зависимости от их уровня и используемых в проектах технологий. Для проектирования ПО нет стандартов и ГОСТов, только вы выбираете, как будете разрабатывать свою программу. Один из лучших способов повысить эффективность работы — применить шаблон проектирования CQRS. Существует три вида паттерна CQRS: Regular, Progressive и Deluxe. В этой статье я расскажу о первом — классическом паттерне Regular CQRS, который мы используем в DD Planet в рамках разработки онлайн-сервиса «Выберу.ру». Progressive и Deluxe — более сложные архитектуры и влекут за собой использование обширного набора абстракций.Я поделюсь опытом своей команды: как мы применили паттерн CQRS в бизнес-приложениях и беспроблемно внедрили его в существующие проекты, не переписывая тысячи строк кода. Классический OnionЧтобы было понятно, для чего нужен паттерн CQRS, сначала рассмотрим, как выглядит классическая архитектура приложения.Классическая «луковая» архитектура состоит из нескольких слоев:
  • Доменный слой — наши сущности и классы.
  • Слой бизнес-логики, где происходит вся обработка доменной логики.
  • Слой приложения — логика самого приложения.
  • Внешние слои: слой 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
Создадим Pipeline для валидации:
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();
       }
   }
}
И зарегистрируем его с помощью DI, добавим инициализацию всех валидаторов для FluentValidation.
namespace CQRS.Sample
{
   public class Startup
   {
       ...
       public void ConfigureServices(IServiceCollection services)
       {
           ...
           services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
           services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
           ...
       }
   }
}
Теперь напишем наш валидатор.
public class AddProductCommandValidator : AbstractValidator<AddProductCommand>
{
   public AddProductCommandValidator()
   {
       RuleFor(c => c.Name).NotEmpty();
       RuleFor(c => c.Alias).NotEmpty();
   }
}
Благодаря возможностям C#, FluentValidation и MediatR нам удалось инкапсулировать логику нашей команды/запроса в рамках одного класса.
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();
           }
       }
   }
}
Это сильно упростило работу с API и решило все основные задачи.На выходе получился красивый инкапсулированный код, понятный всем сотрудникам. Так, мы можем быстро ввести человека в процесс разработки, сократить затраты и время на его реализацию. Текущие результаты можно посмотреть на GitHub.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_sovershennyj_kod (Совершенный код), #_.net, #_api, #_c#, #_mikroservisy (Микросервисы), #_cqrs, #_.net, #_asp.net5, #_patterny (паттерны), #_sovershennyj_kod (
Совершенный код
)
, #_.net, #_api, #_c#, #_mikroservisy (
Микросервисы
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 18:51
Часовой пояс: UTC + 5