The Eventing.Abstractions block exists for one reason: contracts assemblies should not pull in EF Core, RabbitMQ, or the Eventing runtime when they declare an event. This block has zero NuGet dependencies — just five interfaces. It’s the smallest building block in the kit.
What it ships
Five interfaces. That’s it.
public interface IIntegrationEvent{ Guid Id { get; } DateTime OccurredOnUtc { get; } string? TenantId { get; } // null for global, non-tenant-scoped events string CorrelationId { get; } // ties events to requests and traces string Source { get; } // e.g. "Modules.Identity"}
// IIntegrationEventHandler.cspublic interface IIntegrationEventHandler<in TEvent> where TEvent : IIntegrationEvent{ Task HandleAsync(TEvent @event, CancellationToken ct = default);}
// IEventBus.cspublic interface IEventBus{ Task PublishAsync(IIntegrationEvent @event, CancellationToken ct = default); Task PublishAsync(IEnumerable<IIntegrationEvent> events, CancellationToken ct = default);}
// IEventSerializer.cspublic interface IEventSerializer{ string Serialize(IIntegrationEvent @event); IIntegrationEvent? Deserialize(string payload, string eventTypeName);}
// IEventTenantScope.cs — ambient tenant context for event dispatchpublic interface IEventTenantScope{ IDisposable Begin(string? tenantId);}IEventTenantScope is the piece that makes background publishing safe in a multi-tenant world: the bus calls Begin(event.TenantId) before resolving handlers, because a MultiTenantDbContext captures its tenant at construction time. The default implementation is a no-op; the Multitenancy module registers a Finbuckle-backed one.
Pattern: where each interface lives
src/Modules/Notifications/├── Modules.Notifications.Contracts/│ └── (no references except Eventing.Abstractions + Core)└── Modules.Notifications/ └── (references Eventing — gets InMemoryEventBus, OutboxDispatcher, etc.)
src/Modules/Chat/├── Modules.Chat.Contracts/│ ├── IntegrationEvents/MentionedInChannelIntegrationEvent.cs│ │ └── implements IIntegrationEvent (Eventing.Abstractions)│ └── (no reference to Modules.Notifications.Contracts)└── Modules.Chat/ └── publishes via IEventBus (Eventing.Abstractions)When Chat publishes a MentionedInChannelIntegrationEvent, Notifications consumes it through IIntegrationEventHandler<MentionedInChannelIntegrationEvent>. Neither runtime references the other; both just reference the event type declared in Modules.Chat.Contracts. The Eventing runtime (and your event bus of choice) wires the publish-to-subscribe path at runtime.
How modules consume the contracts
Define a custom event in your contracts assembly — this is Chat’s real mention event:
public sealed record MentionedInChannelIntegrationEvent( Guid Id, DateTime OccurredOnUtc, string? TenantId, string CorrelationId, string Source, Guid ChannelId, string? ChannelName, Guid MessageId, string AuthorUserId, string MentionedUserId, string BodyPreview) : IIntegrationEvent;Publish from a handler in the runtime module:
await eventBus.PublishAsync(new MentionedInChannelIntegrationEvent( Id: Guid.NewGuid(), OccurredOnUtc: DateTime.UtcNow, TenantId: currentUser.GetTenant(), CorrelationId: correlationId, Source: "Modules.Chat", /* …payload fields… */), ct).ConfigureAwait(false);Consume from another module’s runtime project:
public sealed class MentionedInChannelNotifyHandler(/* deps */) : IIntegrationEventHandler<MentionedInChannelIntegrationEvent>{ public Task HandleAsync(MentionedInChannelIntegrationEvent evt, CancellationToken ct = default) { // ... }}Gotchas
- No base class.
IIntegrationEventis a pure interface. Use sealed records to implement it; do not invent a base class in this block — it would force every contracts assembly to pull in that base type’s transitive deps. Sourceis a free-form string. Use the canonical module name (Modules.Tickets,Modules.Chat) so logs and diagnostics stay greppable.TenantIdis a nullable string. Global, non-tenant-scoped events carry a nullTenantId; consumers that key on tenant must guard (the Webhooks fanout handler skips null-tenant events entirely). The tenant id also drivesIEventTenantScope— a null id leaves the ambient tenant context unchanged.CorrelationIdis a non-nullable string. It exists to tie events back to the request/trace that caused them — propagate the incoming correlation id, don’t invent a new one mid-chain.- Handler registration is open-generic friendly. The Webhooks module registers
WebhookFanoutHandler<TEvent>against the openIIntegrationEventHandler<>, so one handler type covers every event. If you do this, be deliberate about the side effects.
Critical files
src/BuildingBlocks/Eventing.Abstractions/IIntegrationEvent.cssrc/BuildingBlocks/Eventing.Abstractions/IIntegrationEventHandler.cssrc/BuildingBlocks/Eventing.Abstractions/IEventBus.cssrc/BuildingBlocks/Eventing.Abstractions/IEventSerializer.cssrc/BuildingBlocks/Eventing.Abstractions/IEventTenantScope.cs
Related
- Eventing — the implementation block with
InMemoryEventBus,RabbitMqEventBus, outbox/inbox stores. - Webhooks module — uses the open-generic
IIntegrationEventHandler<>pattern to fan every event to subscribers. - Notifications module — consumes integration events into per-user inbox rows.