.agents/rules/eventing.md
Read before publishing/handling cross-module events. src/BuildingBlocks/Eventing/.
DomainEvent (record: EventId, OccurredOnUtc, CorrelationId, TenantId). Raised on aggregates (IHasDomainEvents).IIntegrationEvent (Id, OccurredOnUtc, TenantId, CorrelationId, Source). Handlers implement IIntegrationEventHandler<T> (single HandleAsync(T, ct)), are sealed, live in Events/ or IntegrationEventHandlers/.Do not call IEventBus directly from a handler. Publish via the outbox so it commits in the same transaction and survives crashes:
await _outboxStore.AddAsync(integrationEvent, ct).ConfigureAwait(false);
EfCoreOutboxStore.AddAsync serializes + SaveChanges immediately. OutboxDispatcherHostedService polls every OutboxDispatchIntervalSeconds (default 10), OutboxDispatcher pulls a batch (OutboxBatchSize, default 100), publishes via IEventBus, and dead-letters after OutboxMaxRetries (default 5) → IsDead. OutboxMessage/InboxMessage are IGlobalEntity (no tenant filter — the dispatcher has no tenant context; TenantId is an explicit column).
InMemoryEventBus resolves handlers in a fresh DI scope and applies the Inbox: skips if IInboxStore.HasProcessedAsync(eventId, handlerName), marks processed after success. Composite key {Id, HandlerName}; concurrent-insert race is swallowed. Don't hand-roll dedup.
ConfigureServices)services.AddEventingCore(builder.Configuration); // serializer + bus + hosted dispatcher
services.AddEventingForDbContext<MyDbContext>(); // outbox/inbox stores (scoped)
services.AddIntegrationEventHandlers(typeof(MyModule).Assembly); // scans IIntegrationEventHandler<>
Bus = EventingOptions.Provider: "RabbitMQ" → RabbitMqEventBus (durable topic exchange); else InMemoryEventBus (default).
Type.GetType() returns null → the message dead-letters. Keep event type names/namespaces stable, or migrate dead rows.IMultiTenantContextSetter (see WebhookFanoutHandler, modules/webhooks.md).UseHostedServiceDispatcher=false to drive the outbox via Hangfire instead of the hosted service.