[Совершенный код, .NET, API, C#, Микросервисы] Паттерн CQRS: теория и практика в рамках ASP.Net Core 5
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Скорость разработки и производительность программистов могут отличаться в зависимости от их уровня и используемых в проектах технологий. Для проектирования ПО нет стандартов и ГОСТов, только вы выбираете, как будете разрабатывать свою программу. Один из лучших способов повысить эффективность работы — применить шаблон проектирования 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
===========
Похожие новости:
- [.NET, C#] Реализация Minecraft Query протокола в .Net Core
- [.NET, C#] IQueryable порождает сильную связанность (перевод)
- [Разработка веб-сайтов, Python, API] Как я сделал веб-фреймворк без MVC — Pipe Framework
- [.NET] First touch of Kafka
- [Python, .NET, История IT] Языку программирования Python исполнилось 30 лет
- [Информационная безопасность, Антивирусная защита, Разработка под MacOS] На 30 тысячах компьютеров с macOS нашли странный зловред, который ждёт команду
- [Программирование, Совершенный код] Что такое хороший код? Считаем звёзды
- [Программирование, .NET, ASP, C#] Реализуем глобальную обработку исключений в ASP.NET Core приложении (перевод)
- [.NET] Как изменить формат данных JSON на Snake Case в ASP.NET Core Web API
- [Программирование, DevOps, Микросервисы] Feature Flags и фабрика ПО
Теги для поиска: #_sovershennyj_kod (Совершенный код), #_.net, #_api, #_c#, #_mikroservisy (Микросервисы), #_cqrs, #_.net, #_asp.net5, #_patterny (паттерны), #_sovershennyj_kod (
Совершенный код
), #_.net, #_api, #_c#, #_mikroservisy (
Микросервисы
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:51
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Скорость разработки и производительность программистов могут отличаться в зависимости от их уровня и используемых в проектах технологий. Для проектирования ПО нет стандартов и ГОСТов, только вы выбираете, как будете разрабатывать свою программу. Один из лучших способов повысить эффективность работы — применить шаблон проектирования CQRS. Существует три вида паттерна CQRS: Regular, Progressive и Deluxe. В этой статье я расскажу о первом — классическом паттерне Regular CQRS, который мы используем в DD Planet в рамках разработки онлайн-сервиса «Выберу.ру». Progressive и Deluxe — более сложные архитектуры и влекут за собой использование обширного набора абстракций.Я поделюсь опытом своей команды: как мы применили паттерн CQRS в бизнес-приложениях и беспроблемно внедрили его в существующие проекты, не переписывая тысячи строк кода. Классический OnionЧтобы было понятно, для чего нужен паттерн CQRS, сначала рассмотрим, как выглядит классическая архитектура приложения.Классическая «луковая» архитектура состоит из нескольких слоев:
Это идеальная архитектура, которая существует множество лет, но она не создает ограничений для связанности компонентов. Так произошло с нашим сайтом «Выберу.ру». Мы получили спагетти-код, в котором связанность была на очень высоком уровне. Новые разработчики приходили в шок, когда его видели. Самое страшное, что могло случиться — введение нового сотрудника в приложение. Объяснить, что и почему, казалось просто невозможным.И мы подошли к моменту, когда перед нами встала важная задача устранить этот недостаток — инкапсулировать доменную логику в одном месте, уменьшить связанность и улучшить связность. Мы начали искать новые паттерны проектирования и остановились на CQRS. CQRS Определение и задачи CQRS (Command Query Responsibility Segregation)— это шаблон проектирования, который разделяет операции на две категории:
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
namespace CQRS.Sample
{ public class Startup { ... public void ConfigureServices(IServiceCollection services) { ... services.AddMediatR(Assembly.GetExecutingAssembly()); services.AddControllers(); ... } } } 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; } } } 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; } } } } 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); } } } dotnet add package FluentValidation.AspNetCore
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(); } } } 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(); } } 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(); } } } } =========== Источник: habr.com =========== Похожие новости:
Совершенный код ), #_.net, #_api, #_c#, #_mikroservisy ( Микросервисы ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:51
Часовой пояс: UTC + 5