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 four interfaces. It’s the smallest building block in the kit.
What it ships
Four interfaces. That’s it.
public interface IIntegrationEvent{ Guid Id { get; } DateTime OccurredOnUtc { get; } Guid? TenantId { get; } Guid? CorrelationId { get; } string Source { get; } // e.g. "Modules.Identity"}
// IIntegrationEventHandler.cspublic interface IIntegrationEventHandler<TEvent> where TEvent : IIntegrationEvent{ ValueTask HandleAsync(TEvent @event, CancellationToken ct);}
// IEventBus.cspublic interface IEventBus{ ValueTask PublishAsync<TEvent>(TEvent @event, CancellationToken ct = default) where TEvent : IIntegrationEvent;}
// IEventSerializer.cspublic interface IEventSerializer{ string Serialize<T>(T @event) where T : IIntegrationEvent; object Deserialize(string json, Type type);}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:
public sealed record TicketResolvedIntegrationEvent( Guid TicketId, long TicketNumber, Guid ReporterUserId, Guid? AssigneeUserId, string? ResolutionNote, Guid Id, DateTime OccurredOnUtc, Guid? TenantId, Guid? CorrelationId) : IIntegrationEvent{ public string Source => "Modules.Tickets";}Publish from a handler in the runtime module:
public sealed class ResolveTicketCommandHandler(ITicketsDbContext db, IEventBus bus, ICurrentUser current) : ICommandHandler<ResolveTicketCommand, Unit>{ public async ValueTask<Unit> Handle(ResolveTicketCommand cmd, CancellationToken ct) { var ticket = await db.Tickets.FindAsync([cmd.TicketId], ct).ConfigureAwait(false); ticket!.Resolve(cmd.ResolutionNote); await db.SaveChangesAsync(ct).ConfigureAwait(false);
await bus.PublishAsync(new TicketResolvedIntegrationEvent( ticket.Id, ticket.Number, ticket.ReporterUserId, ticket.AssignedToUserId, cmd.ResolutionNote, Id: Guid.NewGuid(), OccurredOnUtc: DateTime.UtcNow, TenantId: current.GetTenant(), CorrelationId: /* …if propagating */), ct).ConfigureAwait(false);
return Unit.Value; }}Consume from another module’s runtime project:
public sealed class TicketResolvedNotifyHandler(/* deps */) : IIntegrationEventHandler<TicketResolvedIntegrationEvent>{ public ValueTask HandleAsync(TicketResolvedIntegrationEvent evt, CancellationToken ct) { // ... }}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). The kit’s audit module uses this field for the “Source” column in audit records, so consistency matters.TenantIdis nullable. Platform-wide events (UserRegisteredIntegrationEventfor a SuperAdmin registration, plan-change events) carry a nullTenantId. Consumers that key on tenant must guard. The Webhooks module skips null-tenant events entirely.- Handler registration is open-generic friendly. The Webhooks module registers
IIntegrationEventHandler<>as a generic implementation, so a single 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.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.