Skip to main content
Version: Next

In-Memory Events

The in-memory event bus provides local publish/subscribe within a single process. Events are dispatched synchronously to all registered ISubscriber<TEvent> handlers within the same DI scope. No external infrastructure is required.

Installation

The in-memory event bus is part of RCommon.Core, which is included when you install the main package:

NuGet Package
dotnet add package RCommon.Core

Configuration

Register the in-memory event bus using WithEventHandling<InMemoryEventBusBuilder> on the RCommon builder. Use AddProducer to register the producer and AddSubscriber to wire each handler to its event type:

using RCommon;
using RCommon.EventHandling;
using RCommon.EventHandling.Producers;

builder.Services.AddRCommon()
.WithEventHandling<InMemoryEventBusBuilder>(eventHandling =>
{
eventHandling.AddProducer<PublishWithEventBusEventProducer>();
eventHandling.AddSubscriber<OrderPlaced, OrderPlacedHandler>();
eventHandling.AddSubscriber<OrderShipped, OrderShippedHandler>();
});

PublishWithEventBusEventProducer is the built-in producer that delegates to IEventBus.PublishAsync. Additional subscribers can be registered for the same event type; all of them will be called.

Modular composition

WithEventHandling<InMemoryEventBusBuilder> is cache-aware. When multiple modules call it, the cached InMemoryEventBusBuilder is reused and each configuration delegate runs against the same instance — so subscriber and producer registrations from every module accumulate. AddProducer<T> deduplicates by concrete producer type: if OrderingModule and NotificationsModule both call eventHandling.AddProducer<AuditProducer>(), exactly one AuditProducer descriptor is registered. AddSubscriber<TEvent, THandler> accumulates normally, so multiple modules can register handlers for the same event. See Modular Composition for the full conflict matrix.

Defining an event

Events must implement ISerializableEvent. Use ISyncEvent for sequential dispatch or IAsyncEvent for concurrent dispatch:

using RCommon.Models.Events;

public class OrderPlaced : ISyncEvent
{
public OrderPlaced(Guid orderId, DateTime placedAt)
{
OrderId = orderId;
PlacedAt = placedAt;
}

public OrderPlaced() { }

public Guid OrderId { get; }
public DateTime PlacedAt { get; }
}

Implementing a subscriber

Implement ISubscriber<TEvent> and register the class in the DI container via AddSubscriber:

using RCommon.EventHandling.Subscribers;

public class OrderPlacedHandler : ISubscriber<OrderPlaced>
{
private readonly ILogger<OrderPlacedHandler> _logger;

public OrderPlacedHandler(ILogger<OrderPlacedHandler> logger)
{
_logger = logger;
}

public async Task HandleAsync(OrderPlaced @event, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Order {OrderId} placed at {PlacedAt}", @event.OrderId, @event.PlacedAt);
await Task.CompletedTask;
}
}

Publishing events

Resolve IEventProducer or IEnumerable<IEventProducer> and call ProduceEventAsync. The router ensures the event reaches only the producers subscribed to it:

public class OrderService
{
private readonly IEventRouter _eventRouter;
private readonly IUnitOfWorkFactory _uowFactory;

public OrderService(IEventRouter eventRouter, IUnitOfWorkFactory uowFactory)
{
_eventRouter = eventRouter;
_uowFactory = uowFactory;
}

public async Task PlaceOrderAsync(Order order, CancellationToken cancellationToken)
{
using var uow = _uowFactory.CreateUnitOfWork();
// ... persist the order ...

_eventRouter.AddTransactionalEvent(new OrderPlaced(order.Id, DateTime.UtcNow));
await uow.SaveChangesAsync(cancellationToken);
await _eventRouter.RouteEventsAsync(cancellationToken);
}
}

Via IEventBus (direct, no routing layer)

You can also publish directly through IEventBus without going through the router:

public class NotificationService
{
private readonly IEventBus _eventBus;

public NotificationService(IEventBus eventBus)
{
_eventBus = eventBus;
}

public async Task NotifyAsync(OrderPlaced @event, CancellationToken cancellationToken)
{
await _eventBus.PublishAsync(@event, cancellationToken);
}
}

Dynamic subscriptions

IEventBus supports runtime subscriptions that do not require DI registration. These are resolved via ActivatorUtilities at publish time:

eventBus.Subscribe<OrderPlaced, OrderPlacedHandler>();

// or auto-discover all ISubscriber<T> interfaces on a handler type
eventBus.SubscribeAllHandledEvents<OrderPlacedHandler>();

API summary

TypeDescription
InMemoryEventBusBuilderBuilder used with WithEventHandling<T> to configure the in-memory bus
IEventBusIn-process event bus; PublishAsync, Subscribe, SubscribeAllHandledEvents
InMemoryEventBusDefault IEventBus implementation; resolves handlers from a DI scope
ISubscriber<TEvent>Handler interface; implement HandleAsync
PublishWithEventBusEventProducerIEventProducer that delegates to IEventBus.PublishAsync
IEventRouterQueues transactional events and routes them to registered producers
InMemoryTransactionalEventRouterDefault IEventRouter; registered automatically by AddRCommon
ISyncEventMarker; events produced sequentially
IAsyncEventMarker; events produced concurrently
RCommonRCommon