Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Eventuous/eventuous/llms.txt

Use this file to discover all available pages before exploring further.

Event handlers are the objects that actually react to domain events flowing through a subscription. Each subscription can have any number of handlers attached to it; Eventuous fans every received event out to all registered handlers and collects their results. Handlers are plain classes — they carry no subscription-specific state and can be reused across subscriptions.

IEventHandler

All handlers must implement IEventHandler:
public interface IEventHandler {
    // Name used in logs, traces, and metrics
    string DiagnosticName { get; }

    ValueTask<EventHandlingStatus> HandleEvent(IMessageConsumeContext context);
}
DiagnosticName identifies the handler in telemetry output. HandleEvent receives a context object that carries the deserialized message and its metadata, processes it, and returns a status value.

BaseEventHandler

BaseEventHandler is an abstract convenience class that implements IEventHandler. It sets DiagnosticName to the concrete class name automatically:
public abstract class BaseEventHandler : IEventHandler {
    protected BaseEventHandler() => DiagnosticName = GetType().Name;

    public string DiagnosticName { get; }

    public abstract ValueTask<EventHandlingStatus> HandleEvent(IMessageConsumeContext context);
}
Derive directly from BaseEventHandler when you need complete control over dispatch — for example, when building a projector base class.

EventHandler — typed dispatch

The most common starting point is EventHandler, which extends BaseEventHandler with a typed handler registration mechanism:
public abstract class EventHandler(ITypeMapper? mapper = null) : BaseEventHandler
In your constructor, call On<T> for every event type you want to handle:
protected void On<T>(HandleTypedEvent<T> handler) where T : class;
The HandleTypedEvent<T> delegate is:
public delegate ValueTask HandleTypedEvent<T>(MessageConsumeContext<T> consumeContext)
    where T : class;
Calling On<T> registers an internal dispatch entry. At runtime, HandleEvent looks up the event’s Type in that dictionary and invokes the correct handler. Events whose type has no registered handler return EventHandlingStatus.Ignored without any allocation.
If the event type is not registered in TypeMap, Eventuous logs a warning at startup. Register all domain events using TypeMap.RegisterKnownEventTypes() or individual TypeMap.AddType<T>() calls.

EventHandlingStatus

[Flags]
public enum EventHandlingStatus : short {
    Ignored = 0b_1000,  // handler does not recognise this event type
    Success = 0b_0001,  // event was processed successfully
    Pending = 0b_0010,  // processing started but not yet complete
    Failure = 0b_0011,  // handler encountered an error
    Handled = 0b_0111   // any lower three bits set — event was handled
}
The subscription collects EventHandlingStatus values from all handlers and uses them to decide whether to acknowledge or negatively-acknowledge the event with the broker.

IMessageConsumeContext

Every handler receives an IMessageConsumeContext, which exposes the full envelope around the event:
PropertyTypeDescription
Messageobject?Deserialized event payload
MessageTypestringEvent type name from the stream
ContentTypestringContent-type header (e.g. application/json)
StreamStreamNameStream the event was read from
EventNumberulongPosition within the originating stream
StreamPositionulongPosition in the subscription stream
GlobalPositionulongPosition in the global event log
SequenceulongIn-process sequence number (subscription-local)
CreatedDateTimeWhen the event was written (UTC)
MetadataMetadata?Message metadata
SubscriptionIdstringIdentifier of the owning subscription
CancellationTokenCancellationTokenPropagated from the subscription worker
MessageConsumeContext<T> is the typed variant delivered to HandleTypedEvent<T> delegates. Its Message property is typed as T instead of object.

Writing a handler

Derive from EventHandler, inject your dependencies, and register one On<T> call per event type in the constructor:
public class BookingNotificationHandler : EventHandler {
    readonly IEmailService _email;

    public BookingNotificationHandler(IEmailService email) {
        _email = email;

        On<BookingEvents.V1.RoomBooked>(HandleRoomBooked);
        On<BookingEvents.V1.BookingFullyPaid>(HandleFullyPaid);
    }

    async ValueTask HandleRoomBooked(
        MessageConsumeContext<BookingEvents.V1.RoomBooked> ctx) {
        await _email.SendConfirmation(ctx.Message.GuestId, ctx.Stream.GetId());
    }

    async ValueTask HandleFullyPaid(
        MessageConsumeContext<BookingEvents.V1.BookingFullyPaid> ctx) {
        await _email.SendReceipt(ctx.Stream.GetId());
    }
}
Then register it with the subscription builder:
services.AddSubscription<AllStreamSubscription, AllStreamSubscriptionOptions>(
    "NotificationsSubscription",
    builder => builder
        .UseCheckpointStore<MongoCheckpointStore>()
        .AddEventHandler<BookingNotificationHandler>()
);

Registration options

SubscriptionBuilder provides several overloads for attaching handlers:
// Resolve from DI (registered as keyed singleton by SubscriptionId)
builder.AddEventHandler<MyHandler>();

// Resolve with a custom factory
builder.AddEventHandler<MyHandler>(sp => new MyHandler(sp.GetRequiredService<IDep>()));

// Provide an existing instance directly
builder.AddEventHandler(new MyHandler(dependency));
For wrapping patterns (e.g. retry decorators) use AddCompositionEventHandler:
builder.AddCompositionEventHandler<InnerHandler, RetryHandler>(
    inner => new RetryHandler(inner, maxRetries: 3)
);
Handlers are registered as keyed singletons under the SubscriptionId key, so two subscriptions can both register MyHandler and receive separate instances.

Build docs developers (and LLMs) love