Integration Events, Commands, and Queries in SharedKernel
Publish and consume cross-service messages using IIntegrationEvent, IIntegrationEventBus, IIntegrationCommandBus, and IIntegrationQueryBus in SharedKernel.
Use this file to discover all available pages before exploring further.
In a distributed system built with Domain-Driven Design, two kinds of events exist side by side. Domain events are in-process notifications raised by aggregates and dispatched inside a single bounded context using MediatR’s in-memory notification pipeline. Integration events cross those process boundaries — they are part of the Published Language that lets bounded contexts communicate without sharing a domain model. SharedKernel provides a thin, transport-agnostic abstraction for publishing and consuming integration messages, with a concrete MassTransit/RabbitMQ implementation available in the Presentation layer.
/// <summary>/// Represents an integration command sent through a messaging system./// ⚠️ A bounded context should not send commands to another bounded context/// to enforce state changes — prefer integration events./// </summary>public interface IIntegrationCommand : IIntegrationMessage{}
/// <summary>/// Represents an integration query sent through a messaging system./// ⚠️ A bounded context should not query another bounded context/// to enforce state changes — prefer integration events./// </summary>public interface IIntegrationQuery : IIntegrationMessage{}
Use IIntegrationEventBus to publish integration events from the application layer (typically from a domain-event handler):
public interface IIntegrationEventBus{ Task PublishAsync<T>(T @event, CancellationToken cancellationToken = default) where T : class, IIntegrationEvent;}
public interface IIntegrationQueryBus{ Task<Result<TResponse>> SendAsync<TQuery, TResponse>(TQuery query, CancellationToken cancellationToken = default) where TQuery : class, IIntegrationQuery where TResponse : notnull;}
The concrete implementations of IIntegrationEventBus, IIntegrationCommandBus, and IIntegrationQueryBus use MassTransit over RabbitMQ and are registered in the Presentation Integration layer. See the Presentation — Integration Bus docs for setup and transport configuration.
Declare integration events as immutable records implementing IIntegrationEvent:
public sealed record OrderCreatedIntegrationEvent( Guid Id, string UserId, DateTimeOffset DateOccurredOnUtc, Guid OrderId, Guid CustomerId, decimal Total) : IIntegrationEvent{ public DateTimeOffset? DateDispatchedOnUtc { get; set; }}
Keep integration events small and stable. They form the public contract between services — every property change is a potential breaking change for consumers. Prefer adding new event types over modifying existing ones.
Inject IIntegrationEventBus wherever you need to publish. The most common place is an application-layer domain-event handler:
public sealed class OrderCreatedEventHandler : BaseEventHandler<OrderCreatedEvent>{ private readonly IIntegrationEventBus _integrationEventBus; private readonly IUserContextService _userContextService; public OrderCreatedEventHandler( IIntegrationEventBus integrationEventBus, IUserContextService userContextService) { _integrationEventBus = integrationEventBus; _userContextService = userContextService; } public override async Task HandleAsync( OrderCreatedEvent @event, CancellationToken cancellationToken) { var integrationEvent = new OrderCreatedIntegrationEvent( Id: Guid.NewGuid(), UserId: _userContextService.CurrentContext.UserId, DateOccurredOnUtc: @event.DateOccurredOnUtc, OrderId: @event.OrderId, CustomerId: @event.CustomerId, Total: @event.Total); await _integrationEventBus.PublishAsync(integrationEvent, cancellationToken); }}
BaseEventHandler<TEvent> implements IInMemoryEventHandler<TEvent> which in turn implements INotificationHandler<TEvent> from MediatR, so it participates in the standard in-process event dispatch.
On the subscribing service, implement IBaseIntegrationMessageHandler<TIntegrationMessage> from the Presentation.Integration.Contracts package:
public interface IBaseIntegrationMessageHandler<in TIntegrationMessage> where TIntegrationMessage : IIntegrationMessage{ Task HandleAsync(TIntegrationMessage integrationMessage, CancellationToken cancellationToken);}
For integration events, use the more specific IIntegrationEventHandler<TEvent>:
public interface IIntegrationEventHandler<in TEvent> : IBaseIntegrationMessageHandler<TEvent>, IConsumer<TEvent> where TEvent : class, IIntegrationEvent{}
Example consumer in the Inventory service:
public sealed class OrderCreatedIntegrationEventHandler : IIntegrationEventHandler<OrderCreatedIntegrationEvent>{ private readonly ICommandBus _commandBus; public OrderCreatedIntegrationEventHandler(ICommandBus commandBus) => _commandBus = commandBus; public Task Consume(ConsumeContext<OrderCreatedIntegrationEvent> context) => HandleAsync(context.Message, context.CancellationToken); public async Task HandleAsync( OrderCreatedIntegrationEvent integrationEvent, CancellationToken cancellationToken) { var command = new ReserveStockCommand( integrationEvent.OrderId, integrationEvent.CustomerId); await _commandBus.SendAsync(command, cancellationToken); }}
For integration commands, use IIntegrationCommandHandler<TCommand>:
public interface IIntegrationCommandHandler<in TCommand> : IBaseIntegrationMessageHandler<TCommand>, IConsumer<TCommand> where TCommand : class, IIntegrationCommand{}
IIntegrationEventHandler and IIntegrationCommandHandler both extend MassTransit’s IConsumer<T>. Registration with the MassTransit bus is handled in the Presentation Integration layer. See Presentation — Integration Bus for transport setup.
The recommended pattern is to keep domain events internal and map them to integration events inside application-layer BaseEventHandler<TEvent> implementations. This keeps the domain model free of messaging infrastructure concerns and gives you full control over what data leaves the bounded context.
1
Aggregate raises a domain event
// Inside the Order aggregatethis.RaiseDomainEvent(new OrderCreatedEvent(this.Id, this.CustomerId, this.Total));
2
Domain event is dispatched in-process by MediatR
The infrastructure layer dispatches OrderCreatedEvent notifications after committing the unit of work.
3
Application-layer handler maps and publishes
public sealed class OrderCreatedEventHandler : BaseEventHandler<OrderCreatedEvent>{ // ... (see full example above) public override Task HandleAsync(OrderCreatedEvent @event, CancellationToken ct) => _integrationEventBus.PublishAsync( new OrderCreatedIntegrationEvent(...), ct);}
4
Integration event travels over the bus
MassTransit serialises the event and delivers it to RabbitMQ. Any subscribed service receives and processes it independently.