Skip to main content
Version: Next

Distributed Events

Distributed event handling extends the in-process model to message brokers, enabling events to cross service boundaries. RCommon integrates with MassTransit and Wolverine as transport backends while keeping the application handler code identical to the in-memory case: every handler implements the same ISubscriber<TEvent> interface.

How distributed events work

When a distributed transport is configured, RCommon registers a broker-specific IEventProducer (for example PublishWithMassTransitEventProducer or PublishWithWolverineEventProducer). When your application calls IEventRouter.RouteEventsAsync, the router forwards each event to the producer bound to that event type. The producer then calls the broker's publish or send API.

On the receiving side, the broker delivers the message to a consumer that is registered by RCommon. That consumer resolves the matching ISubscriber<TEvent> from the DI container and calls HandleAsync.

The result is:

  • Producer code calls IEventRouter or IEventProducer — no broker-specific API in application code.
  • Handler code implements ISubscriber<TEvent> — no broker-specific base class or attribute.
  • Routing is managed by the internal EventSubscriptionManager, which tracks which events go to which producers.

Publish vs Send

Both MassTransit and Wolverine support two dispatch patterns:

ProducerPatternDescription
PublishWithMassTransitEventProducerFan-outDelivered to all consumers subscribed to the event type
SendWithMassTransitEventProducerPoint-to-pointDelivered to a single consumer endpoint
PublishWithWolverineEventProducerFan-outDelivered to all Wolverine handlers for the message type
SendWithWolverineEventProducerPoint-to-pointDelivered to a single Wolverine handler endpoint

Use publish (fan-out) for events where multiple downstream services should react. Use send (point-to-point) for command-style messages where exactly one consumer should act.

Event contract requirements

Events that travel across a message broker must implement ISerializableEvent. They should also have a parameterless constructor so the broker can deserialize incoming messages:

using RCommon.Models.Events;

public class OrderShipped : ISyncEvent
{
public OrderShipped(Guid orderId, DateTime shippedAt)
{
OrderId = orderId;
ShippedAt = shippedAt;
}

public OrderShipped() { }

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

Mixing transports

A single application can register multiple event handling builders simultaneously. For example, you can route one event type to the in-memory bus (for local side-effects) and another to MassTransit (for cross-service delivery):

builder.Services.AddRCommon()
.WithEventHandling<InMemoryEventBusBuilder>(local =>
{
local.AddProducer<PublishWithEventBusEventProducer>();
local.AddSubscriber<AuditEvent, AuditEventHandler>();
})
.WithEventHandling<MassTransitEventHandlingBuilder>(mt =>
{
mt.AddProducer<PublishWithMassTransitEventProducer>();
mt.AddSubscriber<OrderShipped, OrderShippedHandler>();
// ... MassTransit transport configuration ...
});

The EventSubscriptionManager ensures that AuditEvent goes only to the in-memory producer and OrderShipped goes only to MassTransit. Each handler still implements ISubscriber<TEvent> — the transport is invisible to handler code.

Guaranteed delivery

For reliable at-least-once delivery guarantees, pair MassTransit or Wolverine with the transactional outbox pattern. See Transactional Outbox for details.

See also

RCommonRCommon