Skip to main content

Eventing pattern: IFTTT-style domain events via RabbitMQ

Mirrors a feature the legacy app already has: whenever a master-data record is created/updated/deleted, publish an event so other services can react. One generic abstraction, not a per-entity mechanism.

The event envelope

Application/Common/DomainEvent.cs — the event envelope (DomainEvent record: EntityType, Action, Key, Actor, OccurredAtUtc, CorrelationId, Changes) and DomainEventAction enum (Created/Updated/Deleted).

record DomainEvent(
string EntityType,
DomainEventAction Action,
string Key,
string? Actor,
DateTime OccurredAtUtc,
string? CorrelationId,
IReadOnlyList<PropertyChange>? Changes
);

enum DomainEventAction { Created, Updated, Deleted }

Deliberately thin — a "something changed, go look" signal, not the full record — so it can't go stale relative to what a consumer re-fetches. Changes is the one exception (audit-trail metadata, not the record itself) — see the Audit-Trail Change Detection page.

Publisher contract and implementation

  • IDomainEventPublisher (Application/Common/Interfaces) — the one shared contract every entity repository calls into: PublishAsync(DomainEvent). Implementations must never let a publish failure fail the caller's write — eventing is a side channel, not part of the write's transaction.
  • RabbitMqDomainEventPublisher (Infrastructure/Messaging) — the RabbitMQ-backed implementation, registered as a singleton (opens one connection/channel lazily on first publish, reuses it after). Publishes to a durable topic exchange and routing key shaped like this:
Exchange: eos.domain-events<QueueSuffix>
Routing key: mas.<entitytype>.<action> (lowercased)
Example: mas.city.updated

Catches and logs every exception from the publish path — never rethrows.

Connection settings come from MAS_SYSCONFIG, not appsettings

The legacy app already stores its own queue connection config there (EnableQueues, QueueType, QueueHost, QueueUserName, QueuePassword, QueueSuffix) alongside its SMTP/Redis/Mongo/Elasticsearch settings — Eos reuses that row rather than inventing a second, Eos-only config mechanism.

Read via ISysConfigRepository.GetQueueSettingsAsync (Application/Common/Interfaces / Infrastructure/Persistence/Repositories/Mas/SysConfigRepository.cs), which is read-only (no write path through Eos for this table) and cached like the other Mas* lookup reads — 1hr TTL, cache key Mas:SysConfig:Queue (see the Reference CRUD Pattern's caching step). The DTO (QueueSettingsDto) is deliberately partial — just the queue fields, not every MAS_SYSCONFIG column — since there's no CRUD/write path for this table yet.

Live toggle vs. restart-required settings

EnableQueues is re-checked on every publish (via RabbitMqDomainEventPublisher.ShouldPublish, a pure predicate against the cached settings), so an operator flipping it off takes effect within the cache's TTL without an app restart. Changing QueueHost/QueueUserName/ QueuePassword, by contrast, only takes effect on the next app restart — the connection itself is opened once and kept. Accepted limitation of this first pilot, not a design goal.

Current status

:::note Pilot entities only — no consumers yet City and Choices are the two pilot entities (CityRepository, ChoiceRepository) — CreateAsync/UpdateAsync/DeleteAsync each publish a DomainEvent after a successful write, built via a pure, unit-testable BuildEvent helper on each repository. DeleteAsync's signature carries no actor on either, so Actor is null on Deleted events.

This has not yet been rolled out to the other territory-gated entities — extend the same shape (inject IDomainEventPublisher, publish after each successful write, reuse BuildEvent's pattern) entity by entity once the pilot proves out end-to-end against a real broker.

No consumer subscribes yet — this is still publish-only. See TASKS.md's "Implement IFTTT consumers" item. :::

Reference files

DomainEvent.cs, IDomainEventPublisher.cs, ISysConfigRepository.cs, QueueSettingsDto.cs, SysConfigRepository.cs, RabbitMqDomainEventPublisher.cs, CityRepository.cs, ChoiceRepository.cs.