C#, Кодогенерация и DDD Часть 3.2 — Добавляем шины, обработчики сообщений и реализацию обработчиков сообщений

от автора

В прошлой статье мы сгенерировали Enpoint-ы WebApi по описанию на основе классов, свойств и атрибутов. В этой статье мы добавим генерацию абстракций EventHandler-ов, работающих с шиной, саму шину, реализацию EventHandler-ов для MassTransit, узнаем во сколько раз больше мы генерируем, чем пишем (на этот раз без ошибки).

(первая часть, вторая часть, третья часть, четвертая часть)

Отправка сообщений в шину

Отправка сообщений реализована одним интерфейсом с одним методом (просто отправить данные).

IMessageBus
using System.Threading.Tasks;  namespace Domain.Common.Interfaces.Infrastructure.MessageBus {     /// <summary>     /// Интерфейс, описывающий работу с шиной     /// </summary>     public interface IMessageBus     {         /// <summary>         /// Отсылает сообщение в шину         /// </summary>         /// <typeparam name="T">Тип сообщения</typeparam>         /// <param name="message">Сообщение</param>         /// <param name="busName">Имя шины</param>         /// <returns>Таск для ожидания</returns>         Task Send<T>(T message);     } } 

Реализация отправки сообщений тоже проста:

Чтение сообщений из шины

У нас есть 2 Generic интерфейса для описания чтения сообщений из шины IEventHandler<T> и BatchEventHandler<T>. Первый обрабатывает сообщения по одному, второй — пачками. Так как это Generic интерфейсы, для удобства регистрации в контейнере и резолва они приведены к единому интерфейсу IEventHandlerBase.

Единый не-Generic интерфейс для Generic — хорошая практика.

IEventHandler<T>
namespace Domain.Common.Interfaces.Application.Events {     /// <summary>     /// Выполняет обработку сообщений     /// </summary>     /// <typeparam name="T">Тип сообщения</typeparam>     public interface IEventHandler<T> : IEventHandlerBase     {         /// <summary>         /// Выполняет обработку сообщения         /// </summary>         /// <param name="message">обьект сообщения</param>         void Handle(T message);     } } 
IBatсhEventHandler<T>
using System.Collections.Generic;  namespace Domain.Common.Interfaces.Application.Events {     /// <summary>     /// Выполняет обработку сообщений     /// </summary>     /// <typeparam name="T">Тип сообщения</typeparam>     public interface IBatсhEventHandler<T> : IEventHandlerBase     {         /// <summary>         /// Выполняет обработку сообщения         /// </summary>         /// <param name="messages">обьекты сообщения</param>         void Handle(List<T> messages);         /// <summary>         /// Размер батча         /// </summary>         int BatchSize { get; set; }     } } 
IEventHandlerBase
namespace Domain.Common.Interfaces.Application.Events {     /// <summary>     /// Базовый интерфейс для обработки сообщений     /// </summary>     public interface IEventHandlerBase     {      } }

Какой ужас, но ведь это синхранные интерфейсы! Какой ужас! Ведь везде async/await и TPL!

Да, зачем мне городить Task/async, если этот метод все равно вызывается в отдельном таске, который ничего не делает? Незачем ).

Генерация

Хотелось бы отметить новые моменты в генерации(которые помогут Вам писать по 2-3 генератора за спринт).

Первое, что хотелось бы отметить, это порядок создания генератора:

1) Сначала напишите класс

2) Затем создайте класс генератора

3) Если классов несколько (а не генерация методов, как для WebApi), сделайте foreach по всем DTO с данными для генерации.

3) Скопируйте написанный класс, замените необходимые места (названия классов, части имен) на данные из DTO для генерации:

internal class Generated_WebApiWithBulkInsert_BatchEventHandler_{entity.requestEntityType.Name} : IConsumer<Batch<{entity.requestEntityType.Name}>>

4) Все, теперь этого boilerplate нет

А за сколько можно написать свой BoilerTemplate?

Если вы будете делать что-то проще (например, BoilerTemplate с WebAPI в 10к RPS):

1) Генерировать все описания для DbContext (еще 1-3 класса)

2) Crud (1 класс сервис 1 класс команда)

3) Объекты для взаимодействия по REST API (в стиле передал id и одно\несколько полей) (1-2 класса)

4) Эндпоинты для CRUD (1 -16 классов)

5) Объекты для описания поиска (1 класс)

6) Поиск (1 класс)

7) Эндпоинты для поиска (1 класс)

Вам понадобится написать еще всего-лишь 10-20 генераторов

(Ну и авторизация).

Это относительно немного.

Генерация нашего BatchEventHandler в Application.Workers (Application)

Тут мы генерируем наше описание обработчика пакета событий. Обработчик простой — Берем источник данных и делаем BatchInsert.

Генератор реализации IBatchEventHandler
using CodeGen.GeneratorBase; using CodeGen.GeneratorBase.Context; using CodeGen.Generators.WebApiWithBulkInsert; using CodeGeneration.GeneratorBase; using System.Collections.Generic;  namespace CodeGen.Generators.RequestEntity.Application.Workers {     class RequestEntityBatchEventHandlerGenerator : CodeGeneratorBase<RequestEntityGeneratorDTO>     {         private ICodeGeneratorScanner<RequestEntityGeneratorDTO> scanner;         public RequestEntityBatchEventHandlerGenerator()          {             place = GeneratorRunPlace.ApplicationWorkers;             scanner = new RequestEntityScanner();         }          public override void Generate(GenerationContext context, List<RequestEntityGeneratorDTO> data)         {             foreach (var entity in data)             {                 context.Project.AddSource($"Generated_WebApiWithBulkInsert_BusWorkers_{entity.requestEntityType.Name}", GenerateBusWorker(entity));             }         }          private string GenerateBusWorker(RequestEntityGeneratorDTO entity)         {             var res = $@" using Domain.Common.Interfaces.Application.Events; using Domain.Common.Interfaces.Infrastructure.DAL; using Microsoft.Extensions.Logging; using {entity.requestEntityType.Namespace}  namespace Application.Workers {{     internal class BatchEventHandler{entity.requestEntityType.Name} : IBatсhEventHandler<{entity.requestEntityType.Name}>     {{         protected IDataSource<{entity.requestEntityType.Name}> _dataSource;         protected ILogger _logger;         public BatchEventHandler{entity.requestEntityType.Name}(IDataSource<{entity.requestEntityType.Name}> dataSource, ILogger logger)          {{              _dataSource = dataSource;             _logger = logger;         }}                  public int BatchSize {{ get; set; }}          public void Handle(List<{entity.requestEntityType.Name}> messages)         {{             try             {{                 _dataSource.BatchInsert(messages);             }}             catch(Exception ex)             {{                 _logger.LogError(ex, null);                 throw;             }}         }}     }} }}   ";             return res;                      }          public override List<RequestEntityGeneratorDTO> Parse(GenerationContext context)         {             return scanner.Scan(context);         }     } } 

Генерация реализации IBatchEventHandler для MassTransit (Infrastructure)

Тут все просто:

1) Генерируем класс, реализующий IConsumer из MassTransit

2) резолвим наш IBatchEventHandler в конструкторе

3) Вызываем наш Handle в Consume, в таске

Генератор реализации IConsumer для MassTransit
using CodeGen.GeneratorBase; using CodeGen.GeneratorBase.Context; using CodeGeneration.GeneratorBase; using System.Collections.Generic;  namespace CodeGen.Generators.WebApiWithBulkInsert.Infrastructure.MessageBus {     class RequestEntityInfrastructureWorkerGenerator : CodeGeneratorBase<RequestEntityGeneratorDTO>     {         private ICodeGeneratorScanner<RequestEntityGeneratorDTO> scanner;         public RequestEntityInfrastructureWorkerGenerator()          {             place = GeneratorRunPlace.InfrastructureMessageBus;             scanner = new RequestEntityScanner();         }          public override void Generate(GenerationContext context, List<RequestEntityGeneratorDTO> data)         {             foreach (var entity in data)             {                 context.Project.AddSource($"Generated_WebApiWithBulkInsert_BatchEventHandler_{entity.requestEntityType.Name}", GenerateBusWorker(entity));             }         }          private string GenerateBusWorker(RequestEntityGeneratorDTO entity)         {             var res = $@" using Domain.Common.Interfaces.Application.Events; using MassTransit; using System; using System.Collections.Generic; using {entity.requestEntityType.Namespace}; using System.Threading.Tasks; using System.Linq; using MassTransit; using System.Text;  namespace Infrastructure.Workers {{     internal class Generated_WebApiWithBulkInsert_BatchEventHandler_{entity.requestEntityType.Name} : IConsumer<Batch<{entity.requestEntityType.Name}>>      {{         protected IBatсhEventHandler<{entity.requestEntityType.Name}> _eventHandler;         public Generated_WebApiWithBulkInsert_BatchEventHandler_{entity.requestEntityType.Name}(IBatсhEventHandler<{entity.requestEntityType.Name}> eventHandler)          {{              _eventHandler = eventHandler;                      }}                  public int BatchSize {{ get; set; }}          public Task Consume(ConsumeContext<Batch<{entity.requestEntityType.Name}>> context)         {{             var items = context.Message.Select(i => i.Message).ToList();              Action<object> action = (object taskParams) =>             {{                 //Получаем исходный обьект                 taskParamsMessage{entity.requestEntityType.Name} services = (taskParamsMessage{entity.requestEntityType.Name})taskParams;                  //Вызываем Handle в нашей абстракции                 services.handler.Handle(services.messages);                            }};              var state = new taskParamsMessage{entity.requestEntityType.Name}()             {{                 handler = _eventHandler,                 messages = items             }};              var task = new Task(action, state);              task.Start();              return task;         }}     }}      internal class taskParamsMessage{entity.requestEntityType.Name}     {{         public List<{entity.requestEntityType.Name}> messages {{ get; set; }}          public IBatсhEventHandler<{entity.requestEntityType.Name}> handler {{ get; set; }}     }} }} ";             return res;          }          public override List<RequestEntityGeneratorDTO> Parse(GenerationContext projectContext)         {             return scanner.Scan(projectContext);         }     } } 

Теперь зарегистрируем все это в контейнере

Регистрация простая, я не стал делать отдельный вспомогательный класс (мне лень).

Просто получаем все неабстрактные классы с интерфейсом и регистрируем.

Регистрируем BatchEventhandler-ы из Application.Workers
using Domain.Common.Interfaces.Application.Events; using Domain.Common.Interfaces.Application.Workers; using Microsoft.Extensions.DependencyInjection; namespace Application.Workers {     public static class ContainerRegistration     {         public static IServiceCollection RegisterWorkers(this IServiceCollection collection)         {             var types = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()                 .Where(t => !t.IsAbstract && !t.IsInterface && t.GetInterfaces().Any(i => i == typeof(IWorker)));              foreach(var type in types)             {                 collection.AddTransient(type);                 collection.AddTransient<IWorker>(sp => (IWorker) sp.GetService(type));             }              return collection;         }            } } 

Остается только вызвать методы регистрации в наших приложениях (cmd сервер и asp.net mvc), и все.

Теперь все наши сгенерированные классы будут работать.

Шина

Конфигурация шины храниться естественно в Infrastructure.MessageBus.

Так же в Infrastructure.Bus хранится код для регистрации всех реализаций IConsumer.

Конфигурация шины выполняется методом ConfigureMessageBus.

Пока конфигурация шины и обработчиков сообщений из шины еще не доделана.

Заключение

Скачать и посмотреть можно тут.

Всего написано 4 генератора (два в этой статье):

1) WebAPI (Infrastructure.Web)

2) DTO для данных WebApi (Application.Common)

3) IBatchEventHandler (Application.Workers)

4) Реализация обработчика батчей через MassTransit (Infrastructure.MessageBus)

Коэффициент кодогенерации (сколько кода пишем руками к количеству сгенерированного):

272/24=11,3

По уровню нагрузки — в день я могу добавлять хоть 10 конечных точек.

Генераторы заняли 400 строк.

На описание среднего Endpoint-а ушло порядка 20 строк.

Вся кодогенерация «окупается» за 2 конечные точки. (Дай бог половина АПИ одного IOT устройства).

В следующей статье я наконец-то запущу Kafka у себя и реализую IDataSource (или сделаю заглушку). И измерю свои 10k RPS :).

И обязательно определюсь, с чем я работаю, с Шиной и сообщениями, или с Событями и обработчиками событий.


ссылка на оригинал статьи https://habr.com/ru/articles/932630/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *