Форум программистов, компьютерный форум, киберфорум
UnmanagedCoder
Войти
Регистрация
Восстановить пароль

Паттерн CQRS в C#

Запись от UnmanagedCoder размещена 17.03.2025 в 17:54
Показов 1392 Комментарии 0

Нажмите на изображение для увеличения
Название: 873f0bda-e2e2-40e0-8199-b27cae6a81e8.jpg
Просмотров: 76
Размер:	153.9 Кб
ID:	10440
Создание сложных корпоративных приложений часто требует нестандартных подходов к архитектуре. Один из таких подходов — паттерн CQRS (Command Query Responsibility Segregation), предлагающий простую, но эффективную идею: разделить операции чтения и записи в системе. Если вы когда-нибудь работали над большим проектом на C#, то наверняка сталкивались с проблемой, когда модель данных становится слишком сложной. Она пытается удовлетворить всем требованиям сразу — и операциям чтения, и операциям записи. В итоге код запутывается, производительность падает, а разработчики начинают страдать головной болью при каждой попытке внести изменения.

CQRS решает эту проблему, разделяя единую модель на две отдельные: одну для команд (изменение данных) и другую для запросов (чтение данных). Это не просто теоретическая концепция, а практический подход, позволяющий построить масштабируемые системы с ясным разделением ответственности.

История CQRS началась в 2010 году, когда Грег Янг впервые предложил этот паттерн, основываясь на принципе разделения команд и запросов (Command-Query Separation, CQS), ранее сформулированном Бертраном Мейером. Однако, в отличие от CQS, который применяется на уровне методов, CQRS поднимает ту же идею на архитектурный уровень. Принцип CQS довольно прост: каждый метод должен быть либо командой, которая выполняет действие и изменяет состояние, либо запросом, который возвращает данные, но не изменяет состояние. Это означает, что методы, меняющие данные, должны быть типа void, а методы, возвращающие данные, не должны производить побочных эффектов.

Вот простой пример из стандартной библиотеки .NET:

C#
1
2
3
List<string> animals = new List<string>();
animals.Add("Тигр");        // Команда: изменяет состояние
int count = animals.Count;  // Запрос: только возвращает данные
Однако в реальной жизни встречаются ситуации, когда строгое соблюдение CQS неудобно или невозможно. Классический пример — метод C# Stack.Pop(), который одновременно удаляет элемент из стека и возвращает его:

C#
1
2
3
4
Stack<string> stack = new Stack<string>();
stack.Push("первый");
stack.Push("второй");
string value = stack.Pop();  // Возвращает "второй" и удаляет его из стека
Такие исключения из правил CQS существуют и вполне оправданы, когда комбинирование операций чтения и записи логично и улучшает юзабилити API.

Какие проблемы решает CQRS? В первую очередь, это проблема несоответствия между оптимальной структурой данных для операций чтения и записи. Модели, хорошо подходящие для изменения данных (с проверкой бизнес-правил, поддержкой инкапсуляции), часто оказываются неэффективными для сложных запросов. И наоборот.

Представьте интернет-магазин. При оформлении заказа нам важны валидация, транзакции и бизнес-логика. А при просмотре каталога товаров нужна высокая производительность запросов, которые часто объединяют данные из разных таблиц. CQRS позволяет оптимизировать обе стороны независимо.

Ещё одна проблема, которую решает CQRS — масштабируемость. В большинстве систем операции чтения выполняются гораздо чаще, чем операции записи. Разделив их, мы можем масштабировать каждую часть системы независимо. Например, развернуть много серверов для обработки запросов и меньше серверов для обработки команд. А что насчёт производительности? Используя CQRS, мы можем создать специализированные представления данных, оптимизированные для конкретных запросов. Это особенно полезно, когда нужно объединять данные из нескольких источников или выполнять сложные вычисления.

Есть ситуации, когда применение этого паттерна только усложнит систему без явных преимуществ:
1. В простых CRUD-приложениях с несложной бизнес-логикой. Если ваше приложение в основном занимается созданием, чтением, обновлением и удалением записей, CQRS может оказаться излишним.
2. В системах с низкой нагрузкой, где производительность не критична, а операции чтения и записи распределены равномерно.
3. В проектах с ограниченными ресурсами разработки, где дополнительная сложность CQRS может превысить потенциальную выгоду.

Как заметил Мартин Фаулер: "CQRS подходит для определённых частей приложения, а не для приложения в целом". Часто имеет смысл применять CQRS только к определённым компонентам системы, оставляя остальные с традиционной архитектурой.

Исследование, проведённое Томасом Плоэгером из Технического университета Эйндховена, показало, что команды, применяющие CQRS в правильном контексте, отмечали улучшение поддерживаемости кода и увеличение производительности системы. Однако те же команды сталкивались с трудностями обучения новых разработчиков и увеличением первоначальной сложности системы.

Прежде чем погрузиться глубже в принципы работы CQRS, важно отметить один ключевой момент: CQRS часто путают с Event Sourcing, хотя это разные паттерны. Event Sourcing касается хранения изменений состояния системы как последовательности событий, в то время как CQRS фокусируется на разделении ответственности между командами и запросами. Эти паттерны хорошо сочетаются, но могут применяться независимо друг от друга.

Принципы работы CQRS



В основе CQRS лежит фундаментальное разделение операций на две категории: команды и запросы. Это разделение позволяет создавать специализированные модели данных, оптимизированные для конкретных сценариев использования. Команда в CQRS — это сообщение, которое изменяет состояние системы. Команды обычно выражаются в повелительном наклонении, например: "СоздатьЗаказ", "ОтменитьПодписку", "ОбновитьПрофильПользователя". Команда содержит всю необходимую информацию для выполнения действия и обычно отправляется в систему асинхронно. Запрос — это сообщение, которое извлекает данные без изменения состояния. Запросы часто начинаются со слова "Получить", например: "ПолучитьСписокЗаказов", "ПолучитьДанныеПользователя". Запрос также содержит необходимые параметры для фильтрации и сортировки данных.

Рассмотрим простой пример из повседневной жизни. Представьте библиотеку. Когда вы возвращаете книгу (команда), библиотекарь обновляет карточку книги и вашу читательскую карточку. Когда вы спрашиваете о наличии книги (запрос), библиотекарь просто проверяет каталог и сообщает результат, не внося никаких изменений. В традиционной архитектуре операции чтения и записи часто смешиваются в рамках одной модели данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CustomerService
{
    public Customer GetCustomer(int id)
    {
        // Чтение из базы данных
        return _repository.GetById(id);
    }
 
    public void UpdateCustomer(Customer customer)
    {
        // Проверка бизнес-правил
        ValidateCustomer(customer);
        
        // Запись в базу данных
        _repository.Update(customer);
    }
}
В этом подходе есть несколько проблем:
1. Модель данных должна удовлетворять как требованиям чтения, так и требованиям записи.
2. Масштабирование чтения и записи происходит вместе.
3. Оптимизация сложных запросов затруднена.

CQRS решает эти проблемы, разделяя модели данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Команда (запись)
public class UpdateCustomerCommand : ICommand
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}
 
public class UpdateCustomerCommandHandler : ICommandHandler<UpdateCustomerCommand>
{
    public void Handle(UpdateCustomerCommand command)
    {
        var customer = _repository.GetById(command.CustomerId);
        customer.Name = command.Name;
        customer.Email = command.Email;
        
        // Валидация и бизнес-правила
        _validator.ValidateAndThrow(customer);
        
        _repository.Update(customer);
    }
}
 
// Запрос (чтение)
public class GetCustomerQuery : IQuery<CustomerDto>
{
    public int CustomerId { get; set; }
}
 
public class GetCustomerQueryHandler : IQueryHandler<GetCustomerQuery, CustomerDto>
{
    public CustomerDto Handle(GetCustomerQuery query)
    {
        // Можно напрямую обращаться к базе через SQL или Dapper
        // для оптимальной производительности
        return _readDb.Query<CustomerDto>(
            "SELECT Id, Name, Email FROM Customers WHERE Id = @Id",
            new { Id = query.CustomerId }
        ).FirstOrDefault();
    }
}
Такое разделение открывает множество возможностей. Например, вы можете:
1. Использовать разные хранилища данных для чтения и записи: например, реляционную БД для записи и NoSQL или кэш для чтения.
2. Оптимизировать схему данных для чтения: денормализовать таблицы, создавать материализованные представления, предварительно вычислять агрегаты.
3. Масштабировать компоненты чтения и записи независимо: увеличивать количество серверов для чтения при росте трафика.
4. Упростить домен-модель: в модели для записи фокусироваться только на бизнес-логике и инвариантах, не думая об оптимизации запросов.

Когда мы разделяем модели для чтения и записи, возникает вопрос: как синхронизировать их данные? Существует несколько стратегий:

1. Непосредственное обновление — самая простая стратегия. После успешной обработки команды обработчик явно обновляет модель для чтения. Эта стратегия проста, но создаёт жёсткую связь между моделями.

C#
1
2
3
4
5
6
7
8
9
10
public void Handle(UpdateCustomerCommand command)
{
    // Обновление модели записи
    var customer = _writeRepository.GetById(command.CustomerId);
    customer.Name = command.Name;
    _writeRepository.Update(customer);
    
    // Обновление модели чтения
    _readRepository.UpdateCustomerName(command.CustomerId, command.Name);
}
2. Событийная синхронизация — более гибкий подход. После обработки команды генерируется событие, которое затем обрабатывается для обновления модели чтения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void Handle(UpdateCustomerCommand command)
{
    // Обновление модели записи
    var customer = _writeRepository.GetById(command.CustomerId);
    customer.Name = command.Name;
    _writeRepository.Update(customer);
    
    // Публикация события
    _eventBus.Publish(new CustomerNameUpdatedEvent
    {
        CustomerId = command.CustomerId,
        NewName = command.Name
    });
}
 
// В другом месте
public void Handle(CustomerNameUpdatedEvent @event)
{
    // Обновление модели чтения
    _readRepository.UpdateCustomerName(@event.CustomerId, @event.NewName);
}
3. Периодическая синхронизация — модель чтения обновляется по расписанию, например, каждые несколько минут. Этот подход подходит для систем, где небольшая задержка в получении актуальных данных приемлема.
4. Восстановление при необходимости — хранение всех событий (Event Sourcing) и перестроение модели для чтения при необходимости. Это самый гибкий, но и самый сложный подход.

При работе с CQRS важно осознавать, что между моделями может существовать временная несогласованность данных (eventual consistency). Это является компромиссом для достижения высокой производительности и масштабируемости. В большинстве реальных приложений такая несогласованность приемлема, если пользователи осведомлены о ней. Например, после размещения заказа в интернет-магазине пользователь может не сразу увидеть его в истории заказов. Важно правильно управлять ожиданиями пользователей, например, показывая сообщения типа "Ваш заказ обрабатывается и скоро появится в истории".

Асинхронная обработка команд является еще одним важным аспектом CQRS. В отличие от традиционной синхронной обработки, где клиент ожидает завершения операции, асинхронный подход позволяет клиенту продолжить работу сразу после отправки команды. Это особенно полезно для длительных операций или при высокой нагрузке на систему. Типичная архитектура асинхронной обработки в CQRS выглядит так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Отправка команды
public async Task<Guid> SendCommand(UpdateCustomerCommand command)
{
    // Генерация уникального идентификатора для отслеживания команды
    var commandId = Guid.NewGuid();
    
    // Сохранение команды в очередь
    await _commandQueue.EnqueueAsync(command, commandId);
    
    // Возврат идентификатора для последующей проверки статуса
    return commandId;
}
 
// Где-то в фоновом процессе
public async Task ProcessCommandsAsync()
{
    while (true)
    {
        var commandMessage = await _commandQueue.DequeueAsync();
        try
        {
            await _commandHandlerResolver.ResolveHandler(commandMessage.Command).HandleAsync();
            await _statusRepository.SetStatusAsync(commandMessage.Id, CommandStatus.Completed);
        }
        catch (Exception ex)
        {
            await _statusRepository.SetStatusAsync(commandMessage.Id, CommandStatus.Failed, ex.Message);
        }
    }
}
Такой подход имеет ряд преимуществ:
  • Улучшенная отзывчивость пользовательского интерфейса.
  • Возможность балансировки нагрузки.
  • Устойчивость к пиковым нагрузкам.
  • Возможность повторной обработки в случае сбоев.

При внедрении CQRS становится особенно важным разговор об идемпотентности — свойстве операции давать одинаковый результат при повторном выполнении. Идемпотентные команды упрощают обработку повторных попыток и дублирующих запросов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
public void Handle(ProcessPaymentCommand command)
{
    // Проверка, была ли команда уже выполнена
    if (_processedCommandIds.Contains(command.CommandId))
        return; // Команда уже была обработана, ничего не делаем
    
    // Обработка платежа
    _paymentService.Process(command.PaymentDetails);
    
    // Сохранение информации о выполненной команде
    _processedCommandIds.Add(command.CommandId);
}
Одной из сложностей CQRS является обеспечение консистентности данных между моделями чтения и записи. Иногда возникают ситуации, когда пользователь выполняет команду, а затем сразу пытается увидеть результаты через запрос, но модель чтения ещё не обновилась.

Существуют различные техники решения этой проблемы:
1. Использование механизма версионирования, когда каждое изменение в системе получает уникальный номер версии. Клиент может запрашивать данные с определенной версии:
C#
1
2
3
4
5
// Выполнение команды
var version = await _commandProcessor.Process(new UpdateCustomerCommand { ... });
 
// Запрос с ожиданием определенной версии
var result = await _queryProcessor.ProcessWithVersion(new GetCustomerQuery { ... }, version);
2. Немедленное обновление кэша чтения для конкретного пользователя после выполнения его команды, даже если глобальная синхронизация произойдет позже.

Применение CQRS часто приводит к переосмыслению пользовательского интерфейса: вместо CRUD-ориентированных форм используется интерфейс, ориентированный на задачи. Каждой бизнес-операции соответствует отдельный экран или функция, что делает пользовательский опыт более интуитивным и соответствующим реальным бизнес-процессам.

CQRS
Добрый день. Подскажите, где можно почитать материал по этой теме, для написания курсовой.

Почему паттерн абстрактная фабрика - паттерн уровня объектов, если в нём могут быть статические отношения?
Взято из Шевчук А., Охрименко Д., Касьянов А. Design Patterns via C#. Приемы объектно-ориентированного проектирования (2015): Почему паттерн...

Паттерн
господа, что такое за зверь - паттерны программирования??? Я прогуглил и про педовикил все, но в итоге ниче так и не понял. Везде говорится, что это...

EAV паттерн
Помогите, пожалуйста, разобраться с созданием БД для хранения товаров магазина. (MS SQL Server) Необходима возможность добавления новых...


Реализация CQRS в C#



Теперь, когда мы разобрались с теоретической частью, перейдем к практической реализации CQRS в C#. Давайте рассмотрим структуру проекта и основные компоненты, необходимые для внедрения этого паттерна. Начнем с создания базовой структуры проекта. Обычно CQRS-приложение на C# включает следующие проекты:

C#
1
2
3
4
5
6
MySolution/
├── Domain/ (домен-модель для команд)
├── Application/ (обработчики команд и запросов)
├── Infrastructure/ (реализация репозиториев, БД)
├── API/ (контроллеры API)
└── ReadModel/ (модели для запросов)
Приступим к определению интерфейсов для команд и запросов. Сначала создадим маркерные интерфейсы, которые будут определять контракты наших сообщений:

C#
1
2
3
// Маркерные интерфейсы
public interface ICommand<TResult> { }
public interface IQuery<TResult> { }
Эти интерфейсы не содержат методов – они служат исключительно для обозначения типа сообщения. Дженерик-параметр TResult определяет тип результата, возвращаемого после обработки команды или запроса.

Теперь определим интерфейсы для обработчиков:

C#
1
2
3
4
5
6
7
8
9
10
11
public interface ICommandHandler<TCommand, TResult> 
    where TCommand : ICommand<TResult>
{
    TResult Handle(TCommand command);
}
 
public interface IQueryHandler<TQuery, TResult> 
    where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}
Эти интерфейсы определяют контракт обработки сообщений. Обработчик команды принимает команду и возвращает результат, а обработчик запроса принимает запрос и возвращает запрошенные данные. Рассмотрим пример реализации простой команды для редактирования информации о клиенте:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EditCustomerInfoCommand : ICommand<Task<ValidationResult>>
{
    public EditCustomerInfoCommand(long id, string firstName, string lastName, int age)
    {
        Id = id;
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }
 
    public long Id { get; }
    public string FirstName { get; }
    public string LastName { get; }
    public int Age { get; }
}
Обратите внимание на иммутабельность команды – все свойства доступны только для чтения, а значения задаются через конструктор. Это хорошая практика, так как команда представляет намерение выполнить действие и не должна изменяться после создания. Далее реализуем обработчик этой команды:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class EditCustomerInfoCommandHandler : 
    ICommandHandler<EditCustomerInfoCommand, Task<ValidationResult>>
{
    private readonly DbContextFactory _dbContextFactory;
 
    public EditCustomerInfoCommandHandler(DbContextFactory dbContextFactory)
    {
        _dbContextFactory = dbContextFactory;
    }
 
    public async Task<ValidationResult> Handle(EditCustomerInfoCommand command)
    {
        using var unitOfWork = new UnitOfWork(_dbContextFactory);
        var customerRepository = new CustomerRepository(unitOfWork);
 
        var customer = await customerRepository.GetByIdAsync(command.Id);
        if (customer == null) 
            return new ValidationResult("Клиент не найден!");
 
        customer.Update(
            command.FirstName,
            command.LastName,
            command.Age
        );
 
        customerRepository.Update(customer);
        await unitOfWork.CommitAsync();
 
        return ValidationResult.Success;
    }
}
В обработчике мы получаем клиента из репозитория, обновляем его данные и сохраняем изменения. Если клиент не найден, возвращаем ошибку валидации. Теперь рассмотрим реализацию запроса для получения списка клиентов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class GetAllCustomersQuery : IQuery<Task<IReadOnlyList<CustomerDto>>> 
{
    // Здесь могут быть параметры фильтрации, сортировки, пагинации
}
 
public class GetAllCustomersQueryHandler : 
    IQueryHandler<GetAllCustomersQuery, Task<IReadOnlyList<CustomerDto>>>
{
    private readonly string _connectionString;
 
    public GetAllCustomersQueryHandler(string connectionString)
    {
        _connectionString = connectionString;
    }
 
    public async Task<IReadOnlyList<CustomerDto>> Handle(GetAllCustomersQuery query)
    {
        using var connection = new SqlConnection(_connectionString);
        await connection.OpenAsync();
        
        // Используем Dapper для прямого запроса к базе
        var customers = await connection.QueryAsync<CustomerDto>(@"
            SELECT c.Id, c.FirstName, c.LastName, c.Age, a.Street, a.City 
            FROM Customers c
            LEFT JOIN Addresses a ON a.CustomerId = c.Id AND a.IsPrimary = 1");
            
        return customers.ToList();
    }
}
Обратите внимание, что в запросе мы используем Dapper для прямого доступа к базе данных, минуя ORM. Это позволяет оптимизировать производительность запроса, получая только необходимые данные в одном запросе.
Для удобства отправки команд и запросов создадим диспетчер сообщений, который будет решать, какой обработчик вызвать:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MessageDispatcher
{
    private readonly IServiceProvider _serviceProvider;
 
    public MessageDispatcher(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
 
    public TResult Dispatch<TResult>(ICommand<TResult> command)
    {
        Type handlerType = typeof(ICommandHandler<,>).MakeGenericType(command.GetType(), typeof(TResult));
        dynamic handler = _serviceProvider.GetService(handlerType);
        return handler.Handle((dynamic)command);
    }
 
    public TResult Dispatch<TResult>(IQuery<TResult> query)
    {
        Type handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult));
        dynamic handler = _serviceProvider.GetService(handlerType);
        return handler.Handle((dynamic)query);
    }
}
Диспетчер использует IServiceProvider из ASP.NET Core для получения нужного обработчика на основе типа сообщения.
Теперь нужно зарегистрировать наши обработчики в DI-контейнере. В методе ConfigureServices класса Startup:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void ConfigureServices(IServiceCollection services)
{
    // Регистрация базовых сервисов
    services.AddSingleton<DbContextFactory>();
    services.AddSingleton<MessageDispatcher>();
    
    // Регистрация обработчиков команд
    services.AddTransient<ICommandHandler<EditCustomerInfoCommand, Task<ValidationResult>>, 
        EditCustomerInfoCommandHandler>();
    
    // Регистрация обработчиков запросов
    services.AddTransient<IQueryHandler<GetAllCustomersQuery, Task<IReadOnlyList<CustomerDto>>>, 
        GetAllCustomersQueryHandler>();
}
Для упрощения регистрации множества обработчиков можно написать вспомогательный метод:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void AddHandlers(this IServiceCollection services)
{
    // Находим все типы, реализующие интерфейсы обработчиков
    var handlerTypes = typeof(ICommand<>).Assembly.GetTypes()
        .Where(t => !t.IsAbstract && !t.IsInterface)
        .Where(t => t.GetInterfaces().Any(i => i.IsGenericType && 
            (i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>) || 
             i.GetGenericTypeDefinition() == typeof(IQueryHandler<,>))))
        .ToList();
 
    foreach (var handlerType in handlerTypes)
    {
        // Получаем информацию об интерфейсе обработчика
        var handlerInterface = handlerType.GetInterfaces().First(i => 
            i.IsGenericType && (
                i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>) || 
                i.GetGenericTypeDefinition() == typeof(IQueryHandler<,>)));
            
        // Регистрируем обработчик
        services.AddTransient(handlerInterface, handlerType);
    }
}
Наконец, давайте добавим контроллер API, который будет использовать наш диспетчер для обработки запросов клиентов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[Route("api/[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
    private readonly MessageDispatcher _dispatcher;
 
    public CustomersController(MessageDispatcher dispatcher)
    {
        _dispatcher = dispatcher;
    }
 
    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var query = new GetAllCustomersQuery();
        var result = await _dispatcher.Dispatch(query);
        return Ok(result);
    }
 
    [HttpPut("{id}")]
    public async Task<IActionResult> Update(long id, UpdateCustomerDto dto)
    {
        var command = new EditCustomerInfoCommand(
            id,
            dto.FirstName,
            dto.LastName,
            dto.Age);
 
        var result = await _dispatcher.Dispatch(command);
 
        if (!result.IsValid)
            return BadRequest(result.Errors);
 
        return NoContent();
    }
}
Одним из популярных подходов к реализации CQRS в .NET является использование библиотеки MediatR. Она значительно упрощает организацию команд, запросов и их обработчиков, предоставляя легкие в использовании абстракции.

Вот как можно модифицировать наш код с использованием MediatR:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Установите пакет: MediatR
// Install-Package MediatR
// Install-Package MediatR.Extensions.Microsoft.DependencyInjection
 
// Команда
public class UpdateCustomerCommand : IRequest<Result>
{
    public long Id { get; }
    public string FirstName { get; }
    public string LastName { get; }
    public int Age { get; }
 
    public UpdateCustomerCommand(long id, string firstName, string lastName, int age)
    {
        Id = id;
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }
}
 
// Обработчик команды
public class UpdateCustomerCommandHandler : IRequestHandler<UpdateCustomerCommand, Result>
{
    private readonly ICustomerRepository _repository;
 
    public UpdateCustomerCommandHandler(ICustomerRepository repository)
    {
        _repository = repository;
    }
 
    public async Task<Result> Handle(UpdateCustomerCommand request, CancellationToken cancellationToken)
    {
        var customer = await _repository.GetByIdAsync(request.Id);
        if (customer == null)
            return Result.Failure("Клиент не найден");
 
        customer.Update(request.FirstName, request.LastName, request.Age);
        await _repository.SaveChangesAsync();
        
        return Result.Success();
    }
}
Регистрация MediatR в DI-контейнере выполняется одной строкой:

C#
1
services.AddMediatR(typeof(Startup).Assembly);
Это автоматически найдет и зарегистрирует все обработчики в указанной сборке.

Практическое применение



CQRS — это не просто теоретический паттерн. Он находит применение в реальных проектах, особенно когда речь идёт о сложных бизнес-доменах с высокими требованиями к производительности и масштабируемости. Давайте рассмотрим типичные сценарии, в которых CQRS действительно блистает.

Прежде всего, CQRS особенно ценен в коллаборативных доменах, где несколько пользователей могут одновременно работать с одними и теми же данными. Представьте систему управления проектами, где десятки команд работают параллельно, обновляя статусы задач, добавляя комментарии и загружая файлы. В таком сценарии традиционная CRUD-модель быстро становится узким местом. Вот как CQRS может помочь при создании системы бронирования билетов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Команда для резервирования места
public class ReserveSeatCommand : ICommand<ReservationResult>
{
    public Guid EventId { get; }
    public Guid SeatId { get; }
    public Guid UserId { get; }
    
    // Конструктор и другие свойства
}
 
// Обработчик команды
public class ReserveSeatCommandHandler : ICommandHandler<ReserveSeatCommand, ReservationResult>
{
    public ReservationResult Handle(ReserveSeatCommand command)
    {
        // 1. Проверяем доступность места (с оптимистичной блокировкой)
        // 2. Резервируем место для пользователя
        // 3. Генерируем событие о резервации
        // 4. Возвращаем результат
    }
}
 
// Запрос для просмотра доступных мест
public class GetAvailableSeatsQuery : IQuery<List<SeatDto>>
{
    public Guid EventId { get; }
}
 
// Обработчик запроса
public class GetAvailableSeatsQueryHandler : IQueryHandler<GetAvailableSeatsQuery, List<SeatDto>>
{
    public List<SeatDto> Handle(GetAvailableSeatsQuery query)
    {
        // Запрос к оптимизированной для чтения модели,
        // возможно кэшированной или денормализованной
    }
}
В этом примере модель чтения может обновляться асинхронно после обработки команды резервирования, что позволяет системе справляться с пиковыми нагрузками, когда тысячи пользователей одновременно пытаются забронировать билеты на популярное мероприятие.

Еще одна область, где CQRS демонстрирует свою ценность — финансовые системы. Представьте банковское приложение, где операции по счетам должны проходить строгую валидацию и аудит, а запросы баланса и истории транзакций выполняются невероятно часто:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Команда для перевода средств
public class TransferFundsCommand : ICommand<TransferResult>
{
    public Guid FromAccountId { get; }
    public Guid ToAccountId { get; }
    public decimal Amount { get; }
    public string Reference { get; }
}
 
// Запрос для проверки баланса
public class GetAccountBalanceQuery : IQuery<AccountBalanceDto>
{
    public Guid AccountId { get; }
}
В такой системе модель команд гарантирует целостность транзакций и соблюдение всех бизнес-правил, в то время как модель запросов оптимизирована для быстрого получения баланса и формирования выписок.

CQRS также отлично работает в системах аналитики и отчетности. Когда требуется обрабатывать сложные отчеты, объединяющие данные из различных источников, традиционные ORM-решения могут страдать от производительности. С CQRS вы можете создать специализированную модель для чтения, оптимизированную именно под задачи аналитики:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class GenerateSalesReportQuery : IQuery<SalesReportDto>
{
    public DateTime StartDate { get; }
    public DateTime EndDate { get; }
    public string[] ProductCategories { get; }
    public string[] Regions { get; }
}
 
public class GenerateSalesReportQueryHandler : IQueryHandler<GenerateSalesReportQuery, SalesReportDto>
{
    private readonly ISalesReportRepository _reportRepository;
    
    public SalesReportDto Handle(GenerateSalesReportQuery query)
    {
        // Использование оптимизированных SQL-запросов или материализованных представлений
        return _reportRepository.GenerateReport(query);
    }
}
Для систем с высокими нагрузками CQRS может быть реализован вместе с шаблоном проектирования Event Sourcing. При таком подходе все изменения состояния сохраняются как последовательность событий, что обеспечивает не только надежный аудит, но и возможность воссоздать состояние системы на любой момент времени:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Событие, сгенерированное после обработки команды
public class OrderPlacedEvent : IEvent
{
    public Guid OrderId { get; }
    public Guid CustomerId { get; }
    public OrderItem[] Items { get; }
    public DateTime PlacedOn { get; }
}
 
// Обработчик события для обновления модели чтения
public class OrderPlacedEventHandler : IEventHandler<OrderPlacedEvent>
{
    private readonly IReadModelRepository _repository;
    
    public void Handle(OrderPlacedEvent @event)
    {
        // Обновление модели чтения
        // Возможно, обновление нескольких представлений для разных запросов
    }
}
При мониторинге и отладке CQRS-приложений особое внимание следует уделять двум аспектам:
1. Статус обработки команд — поскольку многие команды обрабатываются асинхронно, важно иметь механизм отслеживания их статуса.
2. Согласованность данных между моделями — важно контролировать, насколько актуальны данные в модели чтения относительно модели записи.

Для эффективного мониторинга можно использовать сочетание логирования, трассировки и метрик производительности. Например, с помощью Application Insights от Microsoft или других APM-решений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class LoggingCommandHandlerDecorator<TCommand, TResult> : ICommandHandler<TCommand, TResult>
    where TCommand : ICommand<TResult>
{
    private readonly ICommandHandler<TCommand, TResult> _decorated;
    private readonly ITelemetry _telemetry;
    
    public TResult Handle(TCommand command)
    {
        string commandName = typeof(TCommand).Name;
        
        _telemetry.TrackEvent($"Command:{commandName}", new Dictionary<string, string> {
            ["CommandData"] = JsonSerializer.Serialize(command)
        });
        
        var stopwatch = Stopwatch.StartNew();
        try
        {
            var result = _decorated.Handle(command);
            stopwatch.Stop();
            
            _telemetry.TrackMetric($"Command:{commandName}:ExecutionTime", stopwatch.ElapsedMilliseconds);
            return result;
        }
        catch (Exception ex)
        {
            _telemetry.TrackException(ex, new Dictionary<string, string> {
                ["CommandName"] = commandName
            });
            throw;
        }
    }
}
При внедрении CQRS в существующее приложение многие разработчики задаются вопросом: как перейти от монолитной архитектуры к CQRS без полного переписывания системы? Ответ прост — пошаговый, инкрементальный подход.

Начать можно с идентификации ограниченных контекстов в вашем приложении (концепция из DDD), которые больше всего выиграют от применения CQRS. Характерные признаки таких областей: сложная бизнес-логика, высокая нагрузка на операции чтения или значительные различия в требованиях к моделям чтения и записи. Рассмотрим практический пример миграции:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Шаг 1: Выделяем команды и запросы из существующих методов сервиса
// Было:
public class ProductService
{
    public Product GetById(int id) { ... }
    public void UpdatePrice(int id, decimal price) { ... }
}
 
// Стало:
public class GetProductQuery : IQuery<Product>
{
    public int Id { get; set; }
}
 
public class UpdateProductPriceCommand : ICommand
{
    public int Id { get; set; }
    public decimal NewPrice { get; set; }
}
Затем создаём обработчики, которые пока используют существующую инфраструктуру:

C#
1
2
3
4
5
6
7
8
9
public class GetProductQueryHandler : IQueryHandler<GetProductQuery, Product>
{
    private readonly ProductService _legacyService;
    
    public Product Handle(GetProductQuery query)
    {
        return _legacyService.GetById(query.Id);
    }
}
По мере прогресса можно постепенно оптимизировать модель чтения, например, создать денормализованное представление продуктов:

C#
1
2
3
4
5
6
7
8
9
public class ProductReadModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string CategoryName { get; set; }  // Денормализация
    public int StockCount { get; set; }       // Денормализация
    public double AverageRating { get; set; } // Предвычисление
}
К вопросу о масштабировании: один из мощных аспектов CQRS — возможность использовать разные технологии хранения для чтения и записи. Рассмотрим сценарий, где для записи используется реляционная база данных, а для чтения — ElasticSearch:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SearchProductsQueryHandler : IQueryHandler<SearchProductsQuery, ProductSearchResult>
{
    private readonly ElasticClient _elasticClient;
    
    public ProductSearchResult Handle(SearchProductsQuery query)
    {
        var searchResponse = _elasticClient.Search<ProductReadModel>(s => s
            .Query(q => q
                .MultiMatch(mm => mm
                    .Fields(f => f.Field(p => p.Name).Field(p => p.Description))
                    .Query(query.SearchTerm)
                )
            )
            .Sort(so => so.Field(f => f.PopularityScore, SortOrder.Descending))
            .Size(query.PageSize)
            .Skip(query.PageSize * (query.Page - 1))
        );
        
        return new ProductSearchResult
        {
            Products = searchResponse.Documents.ToList(),
            TotalHits = searchResponse.Total
        };
    }
}
Такое решение позволяет масштабировать ElasticSearch для обработки высоких нагрузок на поиск, в то время как реляционная БД обеспечивает целостность данных при операциях записи.
При использовании CQRS важно разработать стратегию тестирования. Хорошая новость в том, что разделение команд и запросов делает модульное тестирование гораздо проще:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Fact]
public void UpdatePriceCommand_ShouldUpdateProductPrice()
{
    // Arrange
    var repository = new Mock<IProductRepository>();
    var product = new Product { Id = 1, Price = 10m };
    repository.Setup(r => r.GetById(1)).Returns(product);
    
    var handler = new UpdateProductPriceCommandHandler(repository.Object);
    var command = new UpdateProductPriceCommand { Id = 1, NewPrice = 15m };
    
    // Act
    handler.Handle(command);
    
    // Assert
    repository.Verify(r => r.Save(It.Is<Product>(p => p.Price == 15m)), Times.Once);
}

Заключение



Паттерн CQRS — это не универсальное решение для всех проектов, но в правильном контексте он становится мощным инструментом для создания масштабируемых, производительных и структурированных приложений. При внедрении CQRS стоит начинать с небольших, изолированных частей системы. Постепенное разделение модели на команды и запросы позволяет получить выгоды от паттерна без риска полного переписывания приложения. Особенно хорошо начать с компонента, который испытывает проблемы с производительностью или масштабируемостью.

Независимо от того, используете ли вы CQRS с Event Sourcing или без него, помните о потенциальных сложностях, связанных с согласованностью данных между моделями. Временная несогласованность может быть приемлемой в большинстве случаев, но некоторые бизнес-сценарии потребуют особого внимания к этому вопросу. Не забывайте, что альтернативных архитектурных подходов тоже немало. Например, при отсутствии явных требований к масштабируемости или сложной бизнес-логики, традиционная N-слойная архитектура с репозиториями и сервисами может оказаться проще и дешевле в реализации.

Выбор между CQRS и другими архитектурными стилями всегда должен опираться на конкретные требования проекта, доступные ресурсы и специфику команды. Как говорил Грег Янг, создатель CQRS, самое интересное в этом паттерне — не сам паттерн, а возможности, которые он открывает.

Паттерн MVC
Здравствуйте, уважаемые форумчане! Написал небольшую программу, следуя шаблону MVC (Model-View-Controller). Посмотрите, пожалуйста, всё ли я...

Паттерн MVC
Здравствуйте! Скажите пожалуйста, где почитать про паттерн MVC

Паттерн Strategy
Реализовать паттерн Стратегия по примерам из теоретического материала. Сслыка на метериал:...

Паттерн MVC
Если на форме, которая является контроллером, есть Label в текст которого при изменении переменной должны выводится данные из переменной. Как дать...

Паттерн Composite
Паттерн Composite. Разработать структуру организации армии в игре фэнтези. Армия может состоять из отрядов эльфов, орков, минотавров, кентавров,...

Regex паттерн
Помогите пожалуйста с паттерном, не могу правильный составить... Имеем вот такой вот html код: &lt;tr...

Паттерн мост
Добрый день! Уважаемые форумчане подкиньте пожалуйста идейку по реализации моста. У меня есть два интерфейса: IDBForm и IManager. Первый...

Паттерн singleton
Показать, что стратегия работает на языке С#. Выручайте ребят, буду благодарна))

Паттерн Delegation
Разработать программу на С#, которая предоставляет информацию о багаже ​​пассажиров авиарейса. Весь багаж находится под контролем багажного...

Паттерн Delegation
Разработать программу, которая предоставляет информацию о багаже ​​пассажиров авиарейса. Весь багаж находится под контролем багажного отделения....

Паттерн Одиночка
Необходимо было с помощью паттерна Фасад посчитать страховой взнос за недвижимость. Классы : квартира, таун-хаус, коттедж. Параметры: срок...

Паттерн наблюдатель
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Observer { class Program ...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Обнаружение объектов в реальном времени на Python с YOLO и OpenCV
AI_Generated 29.04.2025
Компьютерное зрение — одна из самых динамично развивающихся областей искусственного интеллекта. В нашем мире, где визуальная информация стала доминирующим способом коммуникации, способность машин. . .
Эффективные парсеры и токенизаторы строк на C#
UnmanagedCoder 29.04.2025
Обработка текстовых данных — частая задача в программировании, с которой сталкивается почти каждый разработчик. Парсеры и токенизаторы составляют основу множества современных приложений: от. . .
C++ в XXI веке - Эволюция языка и взгляд Бьярне Страуструпа
bytestream 29.04.2025
C++ существует уже более 45 лет с момента его первоначальной концепции. Как и было задумано, он эволюционировал, отвечая на новые вызовы, но многие разработчики продолжают использовать C++ так, будто. . .
Слабые указатели в Go: управление памятью и предотвращение утечек ресурсов
golander 29.04.2025
Управление памятью — один из краеугольных камней разработки высоконагруженных приложений. Го (Go) занимает уникальную нишу в этом вопросе, предоставляя разработчикам автоматическое управление памятью. . .
Разработка кастомных расширений для компилятора C++
NullReferenced 29.04.2025
Создание кастомных расширений для компиляторов C++ — инструмент оптимизации кода, внедрения новых языковых функций и автоматизации задач. Многие разработчики недооценивают гибкость современных. . .
Гайд по обработке исключений в C#
stackOverflow 29.04.2025
Разработка надёжного программного обеспечения невозможна без грамотной обработки исключительных ситуаций. Любая программа, независимо от её размера и сложности, может столкнуться с непредвиденными. . .
Создаем RESTful API с Laravel
Jason-Webb 28.04.2025
REST (Representational State Transfer) — это архитектурный стиль, который определяет набор принципов для создания веб-сервисов. Этот подход к построению API стал стандартом де-факто в современной. . .
Дженерики в C# - продвинутые техники
stackOverflow 28.04.2025
История дженериков началась с простой идеи — создать механизм для разработки типобезопасного кода без потери производительности. До их появления программисты использовали неуклюжие преобразования. . .
Тестирование в Python: PyTest, Mock и лучшие практики TDD
py-thonny 28.04.2025
Тестирование кода играет весомую роль в жизненном цикле разработки программного обеспечения. Для разработчиков Python существует богатый выбор инструментов, позволяющих создавать надёжные и. . .
Работа с PDF в Java с iText
Javaican 28.04.2025
Среди всех форматов PDF (Portable Document Format) заслуженно занимает особое место. Этот формат, созданный компанией Adobe, превратился в универсальный стандарт для обмена документами, не зависящий. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru