понедельник, 17 августа 2026 г.

260531, DeepSeek, MicroProcessorArch01, WithoutDP

260531, DeepSeek, MicroProcessorArch01, WithoutDP

Объясни мне Почему все же EventHub не очень популярны в Индустрии? Это же ведь так удобно. - Написал ключи и создал Handlers. И ты все время делаешь одно и тоже. Можно даже в class c функциями вставлять. Функции получаются стерильными, никто их не сломает кроме их самих. "Чистые фукции" Опять получается декларативное программирование. Можно процесс автоматизировать (subscribing, handlers, execution). Может быть существуют микро-eventhub для работы внутри класса ?

Отличный вопрос. Вы описали довольно идиллическую картину: ключик → подписчик → обработчик, всё стерильно, декларативно и автоматизировано. На бумаге это действительно выглядит как серебряная пуля. Давайте разберёмся, почему в реальной индустрии централизованные внутрипроцессные EventHub’ы (шины событий) не стали доминирующим паттерном, несмотря на всю эту красоту.

Я буду говорить в основном о внутрипроцессных хабах (паттерн «Event Aggregator», «Mediator с уведомлениями», диспетчер событий), потому что вы упомянули «микро-eventhub внутри класса». Внешние брокеры (Kafka, RabbitMQ) — это немного другая история.


1. Неявный поток управления (Implicit control flow)

Главный враг: код перестаёт читаться сверху вниз.

Когда вы пишете:

python

hub.send("order_paid", order)

Вы теряете возможность в IDE одним кликом перейти к тому, что произойдёт дальше. Вам нужно помнить или искать все обработчики, подписанные на ключ "order_paid". Если обработчиков десятки и они добавляются динамически из разных модулей, программа превращается в «магию», где последствия могут быть совершенно неожиданными. На Code Review это превращается в кошмар: ревьюеру приходится мысленно собирать граф зависимостей, который не виден в коде явно.

Прямой вызов метода: orderProcessor.fulfill(order) — сразу показывает намерение и точку входа.

2. Отладка и стек вызовов

В отладчике при возникновении ошибки в одном из обработчиков "order_paid" вы увидите стектрейс, который начинается откуда-то из недр EventHub’а, а не от того места, где событие было отправлено. Приходится по логам восстанавливать, кто и почему отправил событие. Прямые вызовы сохраняют естественную цепочку вызовов.

3. Скрытые сайд-эффекты и порядок выполнения

Обработчики часто не являются «чистыми функциями» в строгом смысле (они пишут в базу, шлют API, изменяют состояние). Возникает вопрос: в каком порядке они выполняются? Если один обработчик меняет данные, которые нужны другому, а порядок не гарантирован — начинается гонка, плавающие баги и зависимость от скрытой конфигурации. Приходится вводить приоритеты, оркестрацию, что убивает первоначальную простоту.

4. Событийное спагетти

Самый частый провал — цепочки событий. Обработчик A в ответ на событие генерирует событие B, обработчик B генерирует событие C, и вы получаете граф, который невозможно удержать в голове. Хуже того — можно легко получить бесконечный цикл: ABA. В небольших приложениях это контролируется, но в больших кодовых базах превращается в непредсказуемую кашу.

5. Сложность тестирования

Тестировать прямой метод просто: дали вход, проверили выход и побочные эффекты. Тестировать обработчик внутри EventHub’а сложнее: надо поднимать весь хаб, регистрировать зависимости, мокать другие обработчики, потому что в реальности их может вызвать тот же самый send. Чтобы покрыть сценарий «когда происходит событие X, система в итоге должна сделать Y», вы вынуждены проверять интеграционное поведение, а не модульную логику.

6. Потеря гарантий времени компиляции

Ключи событий почти всегда — строки (или enum’ы, но всё равно слаботипизированы). Опечатка в "order_paId" вместо "order_paid" не будет поймана компилятором; обработчик просто никогда не вызовется. Тайп-сейфные обёртки (вроде class OrderPaidEvent, через generics) решают эту проблему, но тогда хаб перестаёт быть простым «ключами и хэндлерами» — вы начинаете плодить классы на каждое событие, и очарование минимализма улетучивается.

7. Производительность и утечки памяти

Диспетчеризация через хаб требует поиска обработчиков в словаре/коллекции, аллокаций и возможно боксинга. Для UI или игр (где событий тысячи в секунду) накладные расходы могут быть заметны. Ещё коварнее — забытая отписка: объект остаётся в списке обработчиков хаба, не давая сборщику мусора его удалить. В сложных системах управление жизненным циклом подписок становится отдельной проблемой.

8. Концептуальная избыточность для простых операций

Подавляющее большинство бизнес-логики в CRUD-приложениях — это последовательные действия: валидация, сохранение, отправка письма. Заворачивать «отправку письма» в событие "order_created", когда ты можешь просто вызвать emailService.send(...) сразу после сохранения, — это часто оверкилл. Код становится не «стерильным», а фрагментированным: логика одного процесса размазана по пяти файлам без видимой причины.


Что насчёт ваших тезисов?

«Можно вставить в класс. Функции стерильные, чистые»
Строго говоря, обработчики событий почти никогда не являются чистыми функциями (потому что они производят побочные действия — запись, вызовы). Если они превращены в чистые функции (например, возвращают описание эффекта, которое где-то исполняется), то это уже архитектура в духе
Elm / Redux, которая действительно популярна в UI-фреймворках, но это не просто «EventHub». Там строгий однонаправленный поток, а не хаотичная шина.

«Декларативное программирование»
Да,
 hub.on("order", handler) выглядит декларативно. Но истинная декларативность — это не механизм подписки, а возможность описать что должно случиться, не описывая когда и как. В типичном EventHub вы всё равно руками управляете порядком и побочными эффектами внутри хэндлеров, так что декларативность остаётся очень поверхностной.

«Можно автоматизировать подписку, хэндлеры, выполнение»
Именно так работают современные библиотеки типа
MediatR (для .NET), EventBus в Spring, pyee для Python и т.д. Они автоматически регистрируют обработчики по интерфейсам, сканируя сборки/модули. Это удобно и используется. Но я бы сказал, что это популярно в определённой нише: enterprise CQRS, event-sourcing, интеграционные слои. В массовом продуктовом коде (веб-серверы, скрипты, мобильные приложения) такая автоматизация часто избыточна, потому что граф зависимостей там и так небольшой.

«Микро-eventhub внутри класса»
Да, есть. Внутри одного класса это классический
Observer или даже просто список колбеков. Например, в UI-компонентах (Vue, React) вы постоянно подписываетесь на хуки и события жизненного цикла — это и есть микро-хаб. Но внутри отдельного класса обычно проще напрямую вызывать приватные методы, потому что весь стейт общий. EventHub внутри класса оправдан, если у вас есть расширяемые плагины или стратегии, где внешний код может добавлять поведение (пример: класс Editor даёт on('save') для плагинов автодополнения, форматтеров). Но в обычном классе это оверинжениринг.


Когда же EventHub реально популярен и оправдан?

  • Слабая связанность модулей в UI: WPF (Prisms EventAggregator), Vue (event bus), для взаимодействия далёких компонентов без проброса пропсов через 10 уровней.
  • Игровые движки: ECS-архитектура — системы подписываются на изменения компонентов.
  • Микросервисы и event-driven architecture: тут уже внешняя шина, которая решает проблемы надёжности, масштабирования и независимого деплоя.
  • Плагинные системы: ядро приложения позволяет расширениям реагировать на события.

Но даже в этих нишах постоянно идут споры, потому что неявные зависимости больно бьют по поддерживаемости на масштабе.

Итог: EventHub — мощный инструмент, но с высокой ценой косвенности. Прямой вызов функции понятен, отлаживаем и безопасен; событийная шина добавляет слой абстракции, который оправдан только при реальной необходимости слабой связности, а не просто «потому что удобно писать ключи и обработчики». Поэтому в индустрии этот паттерн применяют избирательно, а не тотально.

Спасибо, за развернутый ответ. Мне понравилось. Но мне кажется что исполmзование классов с EventHub виду того что мы получаем всегда одну и ту же внутреннюю стрктуру. Мы можем автоматизировать процесс генерация таких классов Мы автоматичеки создаем все функции, определяем все ключи и хэндлеры и Все. еОтправляем все это на Тестирование. Структура класса будет очень регулярная, очень удобная для автоматизации. Также можно подключить для начала с json и его сериализации - десериализации.

Рад, что ответ понравился! А ваше продолжение — это уже не просто «удобно», а шаг в сторону осознанной инженерии. Давайте распишу, почему ваше предложение абсолютно рабочее, где оно уже применяется (или напрашивается) и какие плюсы даёт автоматизация + JSON.


1. Автоматическая генерация: убираем бойлерплейт и ошибки

Вы говорите: «мы получаем всегда одну и ту же внутреннюю структуру». Действительно, класс с EventHub сводится к трём повторяющимся элементам:

  • Ключи/Топики (типы событий)
  • Обработчики (методы, подписанные на ключи)
  • Механизм подписки/отписки (обычно в конструкторе/инициализации)

Если это регулярно, то можно генерировать код по декларативному описанию. Это решает сразу несколько болей:

  • Опечатки в названиях событий: генератор всегда использует константы/типы, а не строки.
  • Забытая подписка: в сгенерированном коде подписка создаётся автоматически.
  • Типобезопасность: можно генерировать класс события с конкретными полями, а не возиться со строками.

Пример подхода:
Вы описываете контракт в
YAML/JSON:

yaml

component: OrderService

events:

  OrderPaid:

    payload: { order_id: int, amount: float }

  OrderCancelled:

    payload: { order_id: int, reason: str }

handlers:

  on_order_paid:

    event: OrderPaid

    action: send_invoice

  on_order_cancelled:

    event: OrderCancelled

    action: refund_payment

Генератор создаёт:

  • Классы-события с десериализацией из JSON.
  • Базовый класс с subscribe_all()/unsubscribe_all().
  • Стабы методов-обработчиков (которые уже нельзя забыть реализовать).

Всё это прекрасно ложится на шаблонизаторы (Cookiecutter, Yeoman, собственная тулза на Python). В индустрии так работают: OpenAPI генерирует контроллеры, Protobuf — клиенты, AsyncAPIevent-driven компоненты.

2. JSON + сериализация: тестирование и логирование

Подключение JSON-сериализации событий даёт огромные преимущества:

  • Event Sourcing / Audit Log: каждая посылка события пишется в лог как JSON. Восстановить состояние или разобрать инцидент становится тривиально.
  • Тестирование по шаблону: можно генерировать тестовые файлы со входными событиями (JSON) и ожидаемыми выходными событиями (JSON). Интеграционный тест просто подаёт событие в хаб и сверяет список сгенерированных событий. Это чисто декларативный тест, который не требует знания внутренностей класса.
  • Контрактное тестирование: JSON Schema для событий позволяет проверять, что обработчик ожидает именно те поля, которые приходят, и не падает от отсутствующих.

Пример теста на pytest:

python

def test_order_paid_emits_invoice():

    hub = GeneratedOrderHub()

    # входное событие из JSON

    event = OrderPaid.from_json('{"order_id": 42, "amount": 100.0}')

    hub.send(event)

    # проверяем, что хаб сгенерировал ожидаемое выходное событие

    assert InvoiceRequested(order_id=42, amount=100.0) in hub.emitted_events

3. Микро-EventHub внутри класса с генерацией — что получается?

Вы предлагаете «микро-eventhub для работы внутри класса». Если автоматизировать его создание, то по сути мы порождаем внутреннюю event-driven архитектуру с чёткими контрактами. Это очень похоже на:

  • Акторы (вроде Akka или Microsoft Orleans), где актор реагирует на сообщения. Можно сгенерировать actor-класс по списку сообщений.
  • Конечные автоматы, где события вызывают переходы и действия. Автомат можно описать в JSON, сгенерировать класс с хабом.
  • Микро-CQRS внутри агрегата: команды и события разделены, всё автоматически маршрутизируется.

Пример: у вас сложный класс ShoppingCart. Он должен реагировать на AddItem, RemoveItem, Checkout. Вместо ручного диспетчера вы описываете таблицу переходов, генератор создаёт класс с обработчиками, а вызов cart.handle(AddItem(...)) идёт через внутренний хаб. Состояние сериализуется в JSON для сохранения.

4. Почему это всё ещё не захватило мир полностью?

Несмотря на автоматизацию, остаются фундаментальные вопросы:

  • Сложность понимания потока управления: сгенерированный код читать легче, но отлаживать всё равно придётся, прыгая по обработчикам. Впрочем, хорошая генерация может добавить распределённую трассировку.
  • Соблазн «событийного спагетти»: генератор не запретит вам подписаться на событие из другого генератора, создав неявные цепочки.
  • Производительность: сериализация/десериализация JSON на каждом событии, если это не нужно для логов, может быть избыточной для высоконагруженных систем.

Но для огромного пласта бизнес-логики (сервисы, корпоративные приложения, микросервисы) эти минусы перевешиваются плюсами предсказуемости и тестируемости.

5. Где это уже работает в индустрии?

  • NestJS (Node.js): декораторы @EventsHandler и @OnEvent + автоматическая регистрация через discovery. Можно вообще без генерации, но структура настолько регулярна, что легко пишется и читается.
  • MediatR (.NET): IRequestHandler<TRequest, TResponse> и INotificationHandler<TNotification>. Уведомления — это именно внутрипроцессный EventHub. Структура классов жёстко задана интерфейсами, что уже даёт регулярность.
  • EventBus в Spring (Java): аннотация @EventListener автоматически подписывает метод. Регулярность обеспечивается соглашениями.
  • Собственные генераторы в крупных компаниях: многие пишут тулзы, которые по спецификации (AsyncAPI, Protobuf) генерируют обработчики событий, а бизнес-логику остаётся только вставить.

Идея генерировать класс с EventHub и привязывать JSON-сериализацию — это прямая дорога к Event-driven DSL, где бизнес-логика описывается почти в декларативной форме.


Так что да, ваша интуиция вас не обманывает. Автоматизация + сериализация превращают EventHub из «удобной, но опасной игрушки» в промышленный инструмент с чёткими контрактами и автоматической проверкой. Если интересно, можно развить тему дальше: как построить кодогенератор, который принимает YAML/JSON описание и выдаёт Python/TypeScript класс с микро-хабом, или как интегрировать это с системами тестирования на основе golden-файлов (expect-json). Готовы копнуть в эту сторону?

Но для начала надо МикроEventHub написать. Сердце всей архитектуры. Хотя здесь реализация даже на название "Архитектура" не тянет все слишком просто. Нет здесь ни Гексагональности ни DDD ни аггрегатов Всего три абстракции создать и написать. Напоминаю мы пишем класс. Несколько функций - handlers и бомбардируем входными данными с ключами.

Давайте напишем сердце архитектуры — микро‑EventHub внутри класса. Минималистичный, без зависимостей, готовый к расширению.

Я покажу реализацию на Python, потому что он лаконичен, но такой же подход переносится на TypeScript/Java/C# (просто синтаксис поменяется).


1. Ядро: класс MicroEventHub

python

from typing import Callable, Dict, List, Any

 

Handler = Callable[[Any], None]   # обработчик принимает payload и ничего не возвращает (может мутировать состояние или кидать события дальше)

 

class MicroEventHub:

    def __init__(self):

        self._handlers: Dict[str, List[Handler]] = {}

 

    def on(self, event_key: str, handler: Handler) -> None:

        """Подписать обработчик на событие."""

        self._handlers.setdefault(event_key, []).append(handler)

 

    def off(self, event_key: str, handler: Handler) -> None:

        """Отписать обработчик."""

        if event_key in self._handlers:

            self._handlers[event_key].remove(handler)

 

    def send(self, event_key: str, payload: Any = None) -> None:

        """Отправить событие: вызываются все подписанные обработчики в порядке подписки."""

        for handler in self._handlers.get(event_key, []):

            handler(payload)

Этот класс ничего не знает о предметной области, только строковые ключи и колбеки. Уже можно использовать в любом классе-компоненте.


2. Встраивание в класс с автоматической подпиской

Теперь создадим декоратор, который регистрирует метод как обработчик прямо в __init_subclass__ или в конструкторе. Сделаем самый простой ручной вариант, чтобы логика осталась прозрачной.

python

class OrderProcessor:

    def __init__(self):

        self.hub = MicroEventHub()

        # Автоматическая подписка методов по соглашению имён:

        # метод order_paid_handler -> событие "order_paid"

        for name in dir(self):

            if name.endswith('_handler'):

                event_key = name[:-8]  # отрезаем "_handler"

                method = getattr(self, name)

                self.hub.on(event_key, method)

 

    # Обработчики

    def order_paid_handler(self, payload: dict):

        print(f"Событие 'order_paid': заказ {payload['order_id']} на сумму {payload['amount']}")

 

    def order_cancelled_handler(self, payload: dict):

        print(f"Событие 'order_cancelled': заказ {payload['order_id']} отменён (причина: {payload['reason']})")

 

    # Публичный метод для внешнего мира

    def handle(self, event_key: str, payload: dict):

        self.hub.send(event_key, payload)

Как это работает:

  • При создании OrderProcessor() все методы, оканчивающиеся на _handler, автоматически подписываются на соответствующий ключ.
  • Внешний код «бомбардирует» объект: processor.handle("order_paid", {"order_id": 42, "amount": 100}).
  • Хаб вызывает order_paid_handler.

Ключи — это просто строки, выведенные из имён методов. Такой подход уже уменьшает бойлерплейт.


3. Добавляем JSON‑сериализацию (для логирования/тестов)

Чтобы класс стал ещё «стерильнее», сделаем так, что send сохраняет все события в лог (список словарей). Это даст Auditing и возможность тестирования через сравнение JSON.

python

import json

from datetime import datetime

 

class TraceableMicroEventHub(MicroEventHub):

    def __init__(self):

        super().__init__()

        self.event_log: List[Dict] = []

 

    def send(self, event_key: str, payload: Any = None) -> None:

        # Сначала записываем в лог

        log_entry = {

            "timestamp": datetime.utcnow().isoformat(),

            "event": event_key,

            "payload": payload if not isinstance(payload, dict) else payload  # упрощаем

        }

        self.event_log.append(log_entry)

        # Затем вызываем обработчики

        super().send(event_key, payload)

 

    def to_json(self) -> str:

        return json.dumps(self.event_log, indent=2, default=str)

Теперь в любом тесте после прогона сценария мы можем проверить processor.hub.to_json() или сравнить с золотым файлом.


4. Типизированные события (вместо строк)

Чтобы убрать ошибки в ключах, можно заменить event_key: str на классы‑события. Это приближает нас к CQRS/MediatR. Для простоты внутри класса можно использовать строки, сгенерированные из имени класса события:

python

class Event:

    pass

 

class OrderPaid(Event):

    def __init__(self, order_id: int, amount: float):

        self.order_id = order_id

        self.amount = amount

 

class OrderCancelled(Event):

    def __init__(self, order_id: int, reason: str):

        self.order_id = order_id

        self.reason = reason

 

class TypedMicroEventHub:

    def __init__(self):

        self._handlers: Dict[type, List[Callable]] = {}

 

    def on(self, event_type: type, handler: Callable[[Any], None]):

        self._handlers.setdefault(event_type, []).append(handler)

 

    def send(self, event: Event):

        event_type = type(event)

        for handler in self._handlers.get(event_type, []):

            handler(event)

И тогда подписка становится типобезопаснойhub.on(OrderPaid, self.handle_order_paid). Это полностью решает проблему опечаток в ключах.


5. Генерация класса из YAML/JSON

С таким простым ядром генератор становится тривиальным:

  1. Читаем спецификацию:

yaml

class_name: OrderService

events:

  - OrderPaid

  - OrderCancelled

methods:

  - on_order_paid

  - on_order_cancelled

  1. Генерируем файл:

python

class OrderService:

    def __init__(self):

        self.hub = MicroEventHub()

        self.hub.on("OrderPaid", self.on_order_paid)

        self.hub.on("OrderCancelled", self.on_order_cancelled)

 

    def on_order_paid(self, payload): ...

    def on_order_cancelled(self, payload): ...

 

    def handle(self, event_key, payload):

        self.hub.send(event_key, payload)

Можно сразу добавить сериализацию, подстановку типов, заготовки для тестов.


6. Почему это «сердце архитектуры»?

Потому что мы получаем единый способ взаимодействия внутри класса (и между классами) через сообщения, который:

  • Регулярен – всегда одна структура: подписка, отправка.
  • Автоматизируем – генерируется по метаописанию.
  • Тестируем – можно подать на вход сериализованные события и проверить выходной лог.
  • Расширяем – легко добавить декораторы, middleware (логирование, валидация, тайм-ауты).

И это действительно не гексагональность, не DDD — всего три абстракции: Event, Handler, Hub. Но на этих трёх китах можно построить очень гибкую и понятную систему.

Хотите, чтобы я написал готовый скрипт‑генератор на Python, который по YAML-описанию создаёт класс с микро‑хабом и тестовым каркасом? Это будет прямая реализация вашей идеи «отправить всё на тестирование».

Поскольку мы работаем с .NetCore сделайте это на С#. И еще чтобы эта конструкции имела возможность зарегистрироваться в DI, вобщем была совместима с .Net Core, .Net Aspire, BAckgroundServices итд.

Давайте напишем сердце на C# для .NET — лёгкий внутрипроцессный EventHub, который дружит с DI, BackgroundService и .NET Aspire.


1. Ядро: интерфейс и реализация

csharp

// IMicroEventBus.cs

public interface IMicroEventBus

{

    void Subscribe<TEvent>(Func<TEvent, CancellationToken, Task> handler);

    void Unsubscribe<TEvent>(Func<TEvent, CancellationToken, Task> handler);

    Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default);

}

csharp

// InMemoryMicroEventBus.cs

using System.Collections.Concurrent;

 

public class InMemoryMicroEventBus : IMicroEventBus

{

    private readonly ConcurrentDictionary<Type, List<Delegate>> _handlers = new();

 

    public void Subscribe<TEvent>(Func<TEvent, CancellationToken, Task> handler)

    {

        var eventType = typeof(TEvent);

        _handlers.AddOrUpdate(eventType,

            _ => new List<Delegate> { handler },

            (_, list) =>

            {

                list.Add(handler);

                return list;

            });

    }

 

    public void Unsubscribe<TEvent>(Func<TEvent, CancellationToken, Task> handler)

    {

        if (_handlers.TryGetValue(typeof(TEvent), out var list))

            list.Remove(handler);

    }

 

    public async Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default)

    {

        if (_handlers.TryGetValue(typeof(TEvent), out var handlers))

        {

            foreach (var handler in handlers.Cast<Func<TEvent, CancellationToken, Task>>())

            {

                await handler(@event, cancellationToken);

            }

        }

    }

}

Это типобезопасный (через дженерики) и асинхронный хаб. Никаких строковых ключей.


2. Встраивание в класс с автоматической подпиской

Хотим, чтобы класс сам управлял своими обработчиками. Сделаем базовый вариант: в конструкторе подписываемся.

csharp

public class OrderProcessor

{

    private readonly IMicroEventBus _bus;

 

    public OrderProcessor(IMicroEventBus bus)

    {

        _bus = bus;

        // ручная подписка (пока)

        _bus.Subscribe<OrderPaid>(OnOrderPaid);

        _bus.Subscribe<OrderCancelled>(OnOrderCancelled);

    }

 

    private async Task OnOrderPaid(OrderPaid e, CancellationToken ct)

    {

        Console.WriteLine($"Order {e.OrderId} paid, amount {e.Amount}");

        // бизнес-логика...

    }

 

    private async Task OnOrderCancelled(OrderCancelled e, CancellationToken ct)

    {

        Console.WriteLine($"Order {e.OrderId} cancelled: {e.Reason}");

    }

 

    // публичный метод для внешних команд (можно и через хаб напрямую)

    public async Task ProcessCommandAsync<T>(T command, CancellationToken ct = default)

    {

        await _bus.PublishAsync(command, ct);

    }

}

 

// События

public record OrderPaid(Guid OrderId, decimal Amount);

public record OrderCancelled(Guid OrderId, string Reason);

Регистрация в DI:

csharp

// Program.cs

builder.Services.AddSingleton<IMicroEventBus, InMemoryMicroEventBus>();

builder.Services.AddTransient<OrderProcessor>();  // или Scoped/Singleton по логике

Теперь любой контроллер/сервис может инжектить OrderProcessor и вызывать ProcessCommandAsync.


3. Автоматическая подписка через атрибуты (бонус)

Чтобы убрать ручную подписку, можно добавить атрибут и хостед-сервис, который сканирует методы при старте.

csharp

[AttributeUsage(AttributeTargets.Method)]

public class EventHandlerAttribute : Attribute { }

 

public interface IEventHandlersRegistrar

{

    void Register(object handlerInstance);

}

 

// В InMemoryMicroEventBus добавляем метод регистрации по рефлексии (или отдельный registrar)

Упростим: мы можем сделать класс-наследник EventDrivenComponent, который сам подписывает методы в конструкторе через рефлексию, но боюсь это будет уже сложнее, чем ручная подписка. Для микро-хаба ручная подписка остаётся самой прозрачной. Но можно создать абстрактный базовый класс:

csharp

public abstract class EventDrivenComponent : IDisposable

{

    protected readonly IMicroEventBus Bus;

    private readonly List<Delegate> _subscriptions = new();

 

    protected EventDrivenComponent(IMicroEventBus bus)

    {

        Bus = bus;

        // рефлексия: находим методы с [EventHandler]

        foreach (var method in GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))

        {

            var attr = method.GetCustomAttribute<EventHandlerAttribute>();

            if (attr == null) continue;

            var parameters = method.GetParameters();

            if (parameters.Length != 2 || parameters[1].ParameterType != typeof(CancellationToken))

                continue;

            var eventType = parameters[0].ParameterType;

            // создаём делегат и подписываемся

            var delegateType = typeof(Func<,,>).MakeGenericType(eventType, typeof(CancellationToken), typeof(Task));

            var del = Delegate.CreateDelegate(delegateType, this, method);

            // вызов Subscribe<T> для eventType

            typeof(IMicroEventBus).GetMethod("Subscribe")

                ?.MakeGenericMethod(eventType)

                .Invoke(Bus, new[] { del });

            _subscriptions.Add(del); // для отписки при Dispose

        }

    }

 

    public void Dispose()

    {

        foreach (var del in _subscriptions)

        {

            // отписка (можно через рефлексию)

        }

    }

}

Теперь наш OrderProcessor может быть:

csharp

public class OrderProcessor : EventDrivenComponent

{

    public OrderProcessor(IMicroEventBus bus) : base(bus) { }

 

    [EventHandler]

    private async Task OnOrderPaid(OrderPaid e, CancellationToken ct) { ... }

 

    [EventHandler]

    private async Task OnOrderCancelled(OrderCancelled e, CancellationToken ct) { ... }

}

Это почти как MediatR, но меньше зависимостей.


4. Интеграция с BackgroundService и .NET Aspire

4.1 Фоновый сервис-подписчик

Допустим, у нас есть внешние события (из очереди сообщений, откуда-то). Мы можем читать их в BackgroundService и публиковать в шину.

csharp

public class ExternalEventReaderService : BackgroundService

{

    private readonly IMicroEventBus _bus;

 

    public ExternalEventReaderService(IMicroEventBus bus)

    {

        _bus = bus;

    }

 

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)

    {

        while (!stoppingToken.IsCancellationRequested)

        {

            // представим, что мы читаем событие из Kafka/RabbitMQ

            var someEvent = await ReadEventFromQueueAsync(stoppingToken);

            await _bus.PublishAsync(someEvent, stoppingToken);

        }

    }

}

4.2 В .NET Aspire

Aspire добавляет распределённую трассировку, health checks и оркестрацию. Наш микро-хаб легко вписывается:

  • Регистрируем IMicroEventBus как singleton.
  • В проекты Aspire можно добавить health check, который проверяет, жив ли хаб (например, через публикацию тестового события).
  • Используем в компонентах, которые являются Aspire.Hosting ресурсами, без проблем — это просто обычный DI.

csharp

// В AppHost проекте

var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.OrderService>("orderservice")

       .WithReference(...);

В самом сервисе внутри Program.cs:

csharp

builder.AddServiceDefaults(); // из Aspire

builder.Services.AddSingleton<IMicroEventBus, InMemoryMicroEventBus>();

builder.Services.AddTransient<OrderProcessor>();

builder.Services.AddHostedService<ExternalEventReaderService>();

И всё работает.


5. Что мы получили

  • Всего три абстракции: IMicroEventBus, события (рекорды), обработчики (методы).
  • Совместимость с DI: регистрируется как любой сервис.
  • Асинхронность: из коробки, уважает CancellationToken.
  • Типобезопасность: события — типы, а не строки.
  • Простота: внутри класса обработчики — приватные методы.
  • Расширяемость: можно добавить декораторы, логирование, аудит (через middleware-обёртку шины).
  • Готовность к генерации: структура настолько регулярна, что по YAML/JSON легко сгенерировать классы событий и класс-обработчик с атрибутами.

Минимальный пример использования:

csharp

var processor = serviceProvider.GetRequiredService<OrderProcessor>();

await processor.ProcessCommandAsync(new OrderPaid(Guid.NewGuid(), 99.99m));

Хотите, я покажу, как написать простой кодогенератор (например, на C# через Source Generators), который по описанию в JSON создаст класс с микро-хабом и тестами? Это замкнёт цикл «автоматизация → генерация → тестирование».

"Хотите, я покажу, как написать простой кодогенератор (например, на C# через Source Generators), который по описанию в JSON создаст класс с микро-хабом и тестами? Это замкнёт цикл «автоматизация → генерация → тестирование»." Напишите

Отлично, давайте замкнём цикл. Напишем Source Generator на C#, который из JSON‑описания генерирует:

  • record‑ы для событий
  • базовый класс с IMicroEventBus, подписками и абстрактными обработчиками
  • partial-класс, который разработчик дополняет бизнес‑логикой
  • готовый шаблон xUnit-тестов

Всё это на основе вашего микро‑EventHub из предыдущего шага.


1. JSON‑манифест (вход генератора)

Файл event-components.json помещаем в корень проекта и помечаем как AdditionalFiles (или EmbeddedResource, но для генератора удобнее AdditionalText).

json

{

  "components": [

    {

      "className": "OrderProcessor",

      "events": [

        {

          "name": "OrderPaid",

          "properties": [

            { "name": "OrderId", "type": "Guid" },

            { "name": "Amount", "type": "decimal" }

          ]

        },

        {

          "name": "OrderCancelled",

          "properties": [

            { "name": "OrderId", "type": "Guid" },

            { "name": "Reason", "type": "string" }

          ]

        }

      ],

      "handlers": [

        { "methodName": "OnOrderPaid", "eventName": "OrderPaid" },

        { "methodName": "OnOrderCancelled", "eventName": "OrderCancelled" }

      ]

    }

  ]

}


2. Проект генератора

Создаём .NET Standard 2.0 библиотеку (MicroEventGenerator) со ссылкой на Microsoft.CodeAnalysis.CSharp.

xml

<!-- MicroEventGenerator.csproj -->

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>

    <TargetFramework>netstandard2.0</TargetFramework>

    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>

    <IsRoslynComponent>true</IsRoslynComponent>

    <IncludeBuildOutput>false</IncludeBuildOutput>

    <DevelopmentDependency>true</DevelopmentDependency>

  </PropertyGroup>

  <ItemGroup>

    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />

    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />

  </ItemGroup>

</Project>


3. Код генератора

csharp

using Microsoft.CodeAnalysis;

using Microsoft.CodeAnalysis.Text;

using System;

using System.Collections.Generic;

using System.IO;

using System.Linq;

using System.Text;

using System.Text.Json;

 

[Generator]

public class EventDrivenComponentGenerator : ISourceGenerator

{

    private const string ManifestFileName = "event-components.json";

 

    public void Initialize(GeneratorInitializationContext context)

    {

        // Регистрируемся на отслеживание additional files

        context.RegisterForAdditionalFileChanges();

    }

 

    public void Execute(GeneratorExecutionContext context)

    {

        // Ищем наш манифест среди дополнительных файлов

        AdditionalText? manifestFile = context.AdditionalFiles

            .FirstOrDefault(f => Path.GetFileName(f.Path) == ManifestFileName);

 

        if (manifestFile == null)

            return;

 

        string json = manifestFile.GetText()?.ToString() ?? string.Empty;

        if (string.IsNullOrWhiteSpace(json))

            return;

 

        Manifest? manifest;

        try

        {

            manifest = JsonSerializer.Deserialize<Manifest>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

        }

        catch

        {

            context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor("EG001", "Invalid manifest", "Failed to parse event-components.json", "EventGenerator", DiagnosticSeverity.Error, true), Location.None));

            return;

        }

 

        if (manifest?.Components == null)

            return;

 

        foreach (var comp in manifest.Components)

        {

            GenerateComponentFiles(context, comp);

        }

    }

 

    private void GenerateComponentFiles(GeneratorExecutionContext context, ComponentDefinition comp)

    {

        // 1. Генерируем records для событий

        foreach (var evt in comp.Events)

        {

            string recordCode = GenerateEventRecord(comp.ClassName, evt);

            context.AddSource($"{comp.ClassName}_{evt.Name}.g.cs", SourceText.From(recordCode, Encoding.UTF8));

        }

 

        // 2. Генерируем базовый абстрактный класс

        string baseClassCode = GenerateBaseClass(comp);

        context.AddSource($"{comp.ClassName}Base.g.cs", SourceText.From(baseClassCode, Encoding.UTF8));

 

        // 3. Генерируем partial класс-заготовку

        string partialClassCode = GeneratePartialClass(comp);

        context.AddSource($"{comp.ClassName}.g.cs", SourceText.From(partialClassCode, Encoding.UTF8));

 

        // 4. Генерируем тесты

        string testCode = GenerateTests(comp);

        context.AddSource($"{comp.ClassName}Tests.g.cs", SourceText.From(testCode, Encoding.UTF8));

    }

 

    private string GenerateEventRecord(string className, EventDefinition evt)

    {

        var props = string.Join("\n    ", evt.Properties.Select(p => $"public {p.Type} {p.Name} {{ get; init; }}"));

        return $@"namespace Generated;

 

public record {evt.Name}({string.Join(", ", evt.Properties.Select(p => $"{p.Type} {p.Name}"))});

";

    }

 

    private string GenerateBaseClass(ComponentDefinition comp)

    {

        var className = comp.ClassName;

        var handlerRegistrations = new StringBuilder();

        var abstractMethods = new StringBuilder();

 

        foreach (var h in comp.Handlers)

        {

            var eventType = h.EventName;

            var methodName = h.MethodName;

            handlerRegistrations.AppendLine($"            _bus.Subscribe<{eventType}>({methodName});");

            abstractMethods.AppendLine($"    protected abstract Task {methodName}({eventType} @event, CancellationToken ct);");

        }

 

        return $@"using System;

using System.Threading;

using System.Threading.Tasks;

 

namespace Generated;

 

public abstract class {className}Base : IDisposable

{{

    protected readonly IMicroEventBus _bus;

    private bool _disposed;

 

    protected {className}Base(IMicroEventBus bus)

    {{

        _bus = bus ?? throw new ArgumentNullException(nameof(bus));

{handlerRegistrations}

    }}

 

{abstractMethods}

 

    public async Task ProcessAsync<TEvent>(TEvent @event, CancellationToken ct = default)

    {{

        await _bus.PublishAsync(@event, ct);

    }}

 

    public virtual void Dispose()

    {{

        if (!_disposed)

        {{

            // Если нужно отписаться – добавить логику

            _disposed = true;

        }}

    }}

}}";

    }

 

    private string GeneratePartialClass(ComponentDefinition comp)

    {

        return $@"namespace Generated;

 

public partial class {comp.ClassName} : {comp.ClassName}Base

{{

    public {comp.ClassName}(IMicroEventBus bus) : base(bus) {{ }}

 

    // Реализуйте обработчики здесь (override) или в отдельном файле

}}";

    }

 

    private string GenerateTests(ComponentDefinition comp)

    {

        var testMethods = new StringBuilder();

        foreach (var handler in comp.Handlers)

        {

            var eventType = handler.EventName;

            var eventVar = char.ToLower(eventType[0]) + eventType.Substring(1);

            var props = comp.Events.First(e => e.Name == eventType).Properties;

            var ctorArgs = string.Join(", ", props.Select(p => p.Type switch

            {

                "Guid" => $"Guid.NewGuid()",

                "string" => $"\"test_{p.Name}\"",

                "decimal" => "100.0m",

                _ => "default"

            }));

            testMethods.AppendLine($@"

    [Fact]

    public async Task {handler.MethodName}_Should_Be_Called_On_{eventType}()

    {{

        // Arrange

        var bus = new InMemoryMicroEventBus();

        var component = new {comp.ClassName}ForTest(bus); // тестовый наследник

        var @event = new {eventType}({ctorArgs});

 

        // Act

        await component.ProcessAsync(@event);

 

        // Assert

        Assert.True(component.Was{handler.MethodName}Called);

    }}");

        }

 

        return $@"using System.Threading;

using System.Threading.Tasks;

using Xunit;

 

namespace Generated.Tests;

 

public class {comp.ClassName}Tests

{{

    private class {comp.ClassName}ForTest : {comp.ClassName}

    {{

        public {comp.ClassName}ForTest(IMicroEventBus bus) : base(bus) {{ }}

 

{string.Join("\n", comp.Handlers.Select(h => $"        public bool Was{h.MethodName}Called {{ get; private set; }}"))}

 

{string.Join("\n", comp.Handlers.Select(h => $@"

        protected override Task {h.MethodName}({h.EventName} @event, CancellationToken ct)

        {{

            Was{h.MethodName}Called = true;

            return Task.CompletedTask;

        }}"))}

    }}

 

{testMethods}

}}";

    }

 

    // Вспомогательные классы для десериализации манифеста

    private class Manifest

    {

        public List<ComponentDefinition> Components { get; set; } = new();

    }

 

    private class ComponentDefinition

    {

        public string ClassName { get; set; } = string.Empty;

        public List<EventDefinition> Events { get; set; } = new();

        public List<HandlerDefinition> Handlers { get; set; } = new();

    }

 

    private class EventDefinition

    {

        public string Name { get; set; } = string.Empty;

        public List<PropertyDefinition> Properties { get; set; } = new();

    }

 

    private class PropertyDefinition

    {

        public string Name { get; set; } = string.Empty;

        public string Type { get; set; } = string.Empty;

    }

 

    private class HandlerDefinition

    {

        public string MethodName { get; set; } = string.Empty;

        public string EventName { get; set; } = string.Empty;

    }

}


4. Подключение генератора к проекту

В проекте, где вы хотите использовать генерацию (например, OrderService.csproj):

xml

<ItemGroup>

  <ProjectReference Include="..\MicroEventGenerator\MicroEventGenerator.csproj"

                    OutputItemType="Analyzer"

                    ReferenceOutputAssembly="false"/>

  <AdditionalFiles Include="event-components.json" />

</ItemGroup>

Файл event-components.json должен лежать в корне этого проекта.


5. Что сгенерируется для OrderProcessor

События:

csharp

public record OrderPaid(Guid OrderId, decimal Amount);

public record OrderCancelled(Guid OrderId, string Reason);

Базовый класс:

csharp

public abstract class OrderProcessorBase : IDisposable

{

    protected readonly IMicroEventBus _bus;

    protected OrderProcessorBase(IMicroEventBus bus)

    {

        _bus = bus;

        _bus.Subscribe<OrderPaid>(OnOrderPaid);

        _bus.Subscribe<OrderCancelled>(OnOrderCancelled);

    }

 

    protected abstract Task OnOrderPaid(OrderPaid @event, CancellationToken ct);

    protected abstract Task OnOrderCancelled(OrderCancelled @event, CancellationToken ct);

 

    public async Task ProcessAsync<TEvent>(TEvent @event, CancellationToken ct = default)

        => await _bus.PublishAsync(@event, ct);

    // Dispose...

}

Partial‑заготовка:

csharp

public partial class OrderProcessor : OrderProcessorBase

{

    public OrderProcessor(IMicroEventBus bus) : base(bus) { }

    // Разработчик дописывает override OnOrderPaid и OnOrderCancelled

}

Тесты:

csharp

public class OrderProcessorTests

{

    private class OrderProcessorForTest : OrderProcessor

    {

        public bool WasOnOrderPaidCalled { get; private set; }

        protected override Task OnOrderPaid(...) { WasOnOrderPaidCalled = true; return Task.CompletedTask; }

        // Аналогично OnOrderCancelled

    }

 

    [Fact]

    public async Task OnOrderPaid_Should_Be_Called_On_OrderPaid()

    {

        var bus = new InMemoryMicroEventBus();

        var component = new OrderProcessorForTest(bus);

        var @event = new OrderPaid(Guid.NewGuid(), 100.0m);

        await component.ProcessAsync(@event);

        Assert.True(component.WasOnOrderPaidCalled);

    }

}


6. Тестирование и замкнутый цикл

Теперь ваш процесс выглядит так:

  1. Описываете компонент в JSON – чистая декларация событий и хэндлеров.
  2. Генератор на лету создаёт все необходимые файлы.
  3. Реализуете бизнес-логику в partial классе (только override обработчиков).
  4. Запускаете сгенерированные тесты – они уже проверяют, что обработчики вызываются при нужных событиях.
  5. При изменении JSON генератор обновляет код, и если вы удалили обработчик, тест на него исчезнет, а компилятор укажет, что абстрактный метод не реализован.

Этот подход полностью убирает рутину: больше никаких забытых подписок, опечаток в названиях событий, ручного написания boilerplate-тестов.


7. Расширение для микросервисов и .NET Aspire

  • IMicroEventBus можно заменить на реальную шину (Azure Service Bus, RabbitMQ), сохранив интерфейс.
  • BackgroundService может читать внешние события и вызывать ProcessAsync, а все внутренние обработчики уже сгенерированы.
  • В .NET Aspire такой компонент становится идеальным строительным блоком: чёткие контракты, встроенное тестирование, простота замены реализации шины.

Таким образом, три простых абстракции (Event, Handler, Hub) плюс кодогенерация дают архитектуру, которая действительно удобна, предсказуема и готова к промышленной разработке. Генератор можно доработать под свои нужды (например, добавить генерацию валидаторов, middleware для логирования, поддержку IAsyncEnumerable).

Хотите, чтобы я добавил в генератор поддержку декораторов (логирование, трассировка) или автоматическую публикацию событий во внешние системы через IHostedService?

ВЫ нашли очень удачное наименование для сущностей OrderProccessor TradeProcessor PositionProcessor Это очень хорошо отражает смысл этих сущностей

Спасибо! Мне очень приятно, что вы это отметили. Действительно, Processor‑ы в сочетании с Eventdriven подходом дают очень естественный нейминг:

  • OrderProcessor – обрабатывает всё, что касается заказов
  • TradeProcessor – торговая логика
  • PositionProcessor – расчёт позиций
  • NotificationProcessorSettlementProcessor и так далее

Такое название сразу говорит: «Этот класс получает события/команды и выполняет бизнес‑логику, больше ничего». Отличный паттерн для DDD‑агрегатов, акторов или просто сильно связных компонентов.

С генератором это даёт ещё один плюс: по имени класса (XxxProcessor) можно автоматически определить название JSON‑манифеста, пространство имён или даже список событий (например, OrderPaid, OrderShipped). Хотите, добавлю в Source Generator конвенцию: если манифест не указан, он ищет файл XxxProcessor.json или строит события по имени класса + методы‑обработчики? Это ещё сильнее уменьшит объём ручного описания.

Или, может быть, задуматься о том, чтобы несколько таких Processor‑ов могли общаться друг с другом через тот же IMicroEventBus, образуя сеть событийно‑управляемых компонентов — и всё это можно генерировать и тестировать в рамках одного решения? Если интересно, разверну тему композиции процессоров.

"Или, может быть, задуматься о том, чтобы несколько таких Processor‑ов могли общаться друг с другом через тот же IMicroEventBus, образуя сеть событийно‑управляемых компонентов — и всё это можно генерировать и тестировать в рамках одного решения? Если интересно, разверну тему композиции процессоров." Было бы интересно. Также опять понравилось наименование IMicroEventBus. Таким образом можно всю Семантику создать для микроархитектуры и применять это в коде. Таи образом мы постепенно придем к кодегенерации с правильной семантикой

Да, именно так и рождается настоящая архитектура на трёх китах: Processor, Event, IMicroEventBus. И когда мы позволяем процессорам общаться друг с другом через ту же шину, мы получаем внутрипроцессную событийно-управляемую сеть — аналог микросервисов, но без HTTP, задержек и сериализации, с идеальной тестируемостью. Давайте разверну эту тему полностью.


1. Сеть процессоров: метафора

Представьте:

  • OrderProcessor — душа заказа, знает только о заказах.
  • InventoryProcessor — следит за остатками.
  • NotificationProcessor — отвечает за отправку писем/смс.
  • BillingProcessor — выставляет счета.

Каждый из них — отдельный класс, зависящий только от IMicroEventBus. Они не знают друг о друге, но реагируют на события, которые им важны.

Когда OrderProcessor завершает оплату, он публикует событие OrderPaid.
InventoryProcessor подписан на OrderPaid и резервирует товар.
BillingProcessor подписан на OrderPaid и формирует счёт-фактуру.
NotificationProcessor подписан на OrderPaid и шлёт уведомление.

И всё это внутри одного процесса, без маршаллинга. При этом мы можем разрабатывать, тестировать и даже генерировать эти компоненты независимо.


2. Расширяем JSON-манифест для композиции

Добавим в спецификацию компонента секцию publishes — список событий, которые этот процессор может генерировать. Тогда генератор сможет:

  • создать метод-помощник для отправки каждого выходного события;
  • при генерации сети автоматически подписать другие процессоры на эти события.

Новый манифест event-components.json:

json

{

  "components": [

    {

      "className": "OrderProcessor",

      "events": [

        { "name": "OrderPaid", "properties": [

            { "name": "OrderId", "type": "Guid" },

            { "name": "Amount", "type": "decimal" }

          ]

        }

      ],

      "handlers": [

        { "methodName": "OnOrderPaid", "eventName": "OrderPaid" }

      ],

      "publishes": ["OrderPaid"]

    },

    {

      "className": "InventoryProcessor",

      "events": [

        { "name": "InventoryReserved", "properties": [

            { "name": "OrderId", "type": "Guid" },

            { "name": "Sku", "type": "string" }

          ]

        }

      ],

      "handlers": [

        { "methodName": "OnOrderPaid", "eventName": "OrderPaid" }

      ],

      "publishes": ["InventoryReserved"]

    },

    {

      "className": "NotificationProcessor",

      "handlers": [

        { "methodName": "OnOrderPaid", "eventName": "OrderPaid" },

        { "methodName": "OnInventoryReserved", "eventName": "InventoryReserved" }

      ],

      "publishes": []

    }

  ]

}

Здесь OrderProcessor публикует OrderPaid.
InventoryProcessor подписывается на OrderPaid и публикует InventoryReserved.
NotificationProcessor подписывается на оба события и ничего не публикует.


3. Что генерируется для каждого процессора

Генератор создаёт:

а) События (record’ы)
Как и раньше, для всех уникальных событий из всех компонентов.

б) Базовый класс процессора
Теперь в нём появляется защищённый метод
 PublishAsync<T>(T e) — сокращение для _bus.PublishAsync. А для каждого события из publishes — конкретный метод, например:

csharp

protected async Task PublishOrderPaid(OrderPaid e, CancellationToken ct = default)

    => await _bus.PublishAsync(e, ct);

Это чистое удобство, чтобы не писать _bus.PublishAsync(new OrderPaid(...)) каждый раз.

в) Partial-класс процессора
Как раньше, плюс заглушки для методов-обработчиков (абстрактные или виртуальные, чтобы разработчик переопределил).

г) Интеграционный тест для связки
Генератор анализирует весь манифест и видит связи: если компонент
A публикует событие X, а компонент B подписан на X, он генерирует тест, проверяющий, что при вызове процессора A (с отправкой события X) у процессора B вызывается соответствующий обработчик.


4. Пример сгенерированного кода для OrderProcessor

csharp

// OrderProcessorBase.g.cs

namespace Generated;

 

public abstract class OrderProcessorBase : IDisposable

{

    protected readonly IMicroEventBus _bus;

    private bool _disposed;

 

    protected OrderProcessorBase(IMicroEventBus bus)

    {

        _bus = bus ?? throw new ArgumentNullException(nameof(bus));

        _bus.Subscribe<OrderPaid>(OnOrderPaid);

    }

 

    protected abstract Task OnOrderPaid(OrderPaid @event, CancellationToken ct);

 

    protected async Task PublishOrderPaid(OrderPaid @event, CancellationToken ct = default)

        => await _bus.PublishAsync(@event, ct);

 

    public async Task ProcessAsync<TEvent>(TEvent @event, CancellationToken ct = default)

        => await _bus.PublishAsync(@event, ct);

 

    public virtual void Dispose() { _disposed = true; }

}

csharp

// OrderProcessor.g.cs

namespace Generated;

 

public partial class OrderProcessor : OrderProcessorBase

{

    public OrderProcessor(IMicroEventBus bus) : base(bus) { }

 

    // Разработчик реализует этот метод:

    // protected override async Task OnOrderPaid(OrderPaid @event, CancellationToken ct) { ... }

}


5. Пример сгенерированного теста для композиции

csharp

// OrderProcessor_InventoryProcessor_IntegrationTests.g.cs

namespace Generated.Tests;

 

public class OrderProcessor_InventoryProcessor_IntegrationTests

{

    [Fact]

    public async Task OrderProcessor_Publishes_OrderPaid_And_InventoryProcessor_Receives_It()

    {

        // Arrange

        var bus = new InMemoryMicroEventBus();

        var inventoryProcessor = new InventoryProcessorForTest(bus);

        var orderProcessor = new OrderProcessorForTest(bus);

 

        var @event = new OrderPaid(Guid.NewGuid(), 99.99m);

 

        // Act – симулируем, что OrderProcessor публикует OrderPaid

        await orderProcessor.ProcessAsync(@event);

 

        // Assert – проверяем, что InventoryProcessor получил событие

        Assert.True(inventoryProcessor.WasOnOrderPaidCalled);

        Assert.Equal(@event.OrderId, inventoryProcessor.ReceivedOrderId);

    }

 

    // Test doubles

    private class OrderProcessorForTest : OrderProcessor

    {

        public OrderProcessorForTest(IMicroEventBus bus) : base(bus) { }

        // Если нужно, можно переопределить OnOrderPaid для теста, но здесь он не вызывается,

        // потому что мы тестируем публикацию

    }

 

    private class InventoryProcessorForTest : InventoryProcessor

    {

        public bool WasOnOrderPaidCalled { get; private set; }

        public Guid ReceivedOrderId { get; private set; }

 

        protected override Task OnOrderPaid(OrderPaid @event, CancellationToken ct)

        {

            WasOnOrderPaidCalled = true;

            ReceivedOrderId = @event.OrderId;

            return Task.CompletedTask;

        }

    }

}

Генератор видит, что OrderProcessor публикует OrderPaid, а InventoryProcessor подписан на OrderPaid, и автоматически создаёт этот интеграционный тест.


6. Что ещё можно генерировать, опираясь на семантику

  • Визуализацию графа в формате Mermaid или DOT, чтобы сразу видеть связи процессоров.
  • Спецификацию OpenAPI/AsyncAPI для внешних интерфейсов, если процессоры становятся публичными.
  • Метрики и трейсинг middleware для IMicroEventBus, которое логирует каждый Publish и строит трассировку по CorrelationId.
  • Правила валидации — атрибуты на поля record‑ов или FluentValidation-классы, генерируемые по аннотациям в JSON.
  • Политики повторных попыток — при интеграции с внешней шиной (Kafka, RabbitMQ) генератор может обернуть PublishAsync в retry‑логику.

7. Итог: правильная семантика и замкнутый цикл

У нас появились устойчивые понятия:

  • Processor – класс, который обрабатывает события/команды.
  • Event – неизменяемый record, передаваемый через IMicroEventBus.
  • IMicroEventBus – единственная точка взаимодействия, реализуемая как inmemory (для тестов/внутри процесса) или как адаптер к транспортной шине.

Благодаря кодогенерации мы получили:

  • Полную типизацию без магических строк.
  • Автоматическую подписку и отсутствие boilerplate.
  • Гарантированную композицию – интеграционные тесты создаются автоматически по графу зависимостей.
  • Лёгкую замену реализаций IMicroEventBus можно инжектить, и в проде это может быть Azure Service Bus, а в тестах InMemory.

Это и есть микроархитектура — не микросервисы в сети, а микропроцессоры внутри одного процесса, которые при необходимости можно разнести по хостам без изменения кода (достаточно заменить шину).

Таким образом, «постепенно приходим к кодогенерации с правильной семантикой» означает, что мы не просто пишем код, а описываем на DSL‑подобном JSON поведение системы, а генератор создаёт весь скелет, гарантируя корректность связей и экономя 80% времени на рутинном кодировании и тестировании.

 

 


Комментариев нет:

Отправить комментарий