Skip to content

React to an event

Goal: when something happens — a book is returned, an order ships — you need to do something: notify a member, call an external API, or record a follow-up fact. That’s a reactor.

A reactor is a class implementing the marker interface IReactor. You don’t implement a method from it — you write a method whose first parameter is the event type you want to handle, and Chronicle routes matching events to it:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
[EventType]
public record ScenariosReactBookReturned(string Isbn);
public interface IScenariosReactNotificationService
{
Task NotifyNextInLine(EventSourceId bookId);
Task NotifyNextInLine(EventSourceId bookId, string bookTitle);
}
public class ScenariosReactWaitlistNotifier(IScenariosReactNotificationService notifications) : IReactor
{
public async Task BookReturned(ScenariosReactBookReturned @event, EventContext context)
{
// context.EventSourceId is the source the event happened to (the book)
await notifications.NotifyNextInLine(context.EventSourceId);
}
}

Chronicle discovers it by convention — no registration.

A reactor may run more than once for the same event (replay, recovery, redeploy). Make the side effect idempotent — record that it happened and skip if it already did. For a side effect that must never run again during a replay — a welcome email, a payment call — mark the handler (or the whole class) with [OnceOnly] and Chronicle excludes it from replays. See OnceOnly.

Reach for the event first — it carries the truth of what happened, and context.EventSourceId tells you what it happened to. But some reactions genuinely need more state. Say the notification should include the book’s title — that lives in the read model, not the event.

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
public record ScenariosReactBook(string Title);
public class ScenariosReactWaitlistNotifierWithTitle(IEventStore eventStore, IScenariosReactNotificationService notifications) : IReactor
{
public async Task BookReturned(ScenariosReactBookReturned @event, EventContext context)
{
// Strongly consistent — rebuilt from the event log, includes this event
var book = await eventStore.ReadModels.GetInstanceById<ScenariosReactBook>(context.EventSourceId);
await notifications.NotifyNextInLine(context.EventSourceId, book.Title);
}
}

The full menu of read APIs — single, all, paged, observed — is in Get read models.

The common “translation” pattern — react to one slice’s event by recording a new fact. The simplest way is to return the event: Chronicle appends it to the event log for you, against the triggering event’s EventSourceId:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
[EventType]
public record ScenariosReactBookReserved(string Isbn);
[EventType]
public record ScenariosReactStockDecreased(string Isbn, int Quantity);
public class ScenariosReactStockKeeping : IReactor
{
public ScenariosReactStockDecreased BookReserved(ScenariosReactBookReserved @event, EventContext context) =>
new(@event.Isbn, 1);
}

Task<StockDecreased> and collections of events work too, and you can control the target event source — see Returning side effects. Auto-appending a returned event is C#-only — every other client appends explicitly instead, shown next.

When you want to inspect the outcome yourself, append explicitly and handle the AppendResult — and throw if it failed, so the partition pauses instead of the fact being silently lost:

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
public class ScenariosReactStockCouldNotBeDecreased(string isbn) : Exception($"Stock could not be decreased for ISBN {isbn}");
public class ScenariosReactStockKeepingExplicit(IEventStore eventStore) : IReactor
{
public async Task BookReserved(ScenariosReactBookReserved @event, EventContext context)
{
var result = await eventStore.EventLog.Append(
context.EventSourceId, new ScenariosReactStockDecreased(@event.Isbn, 1));
if (!result.IsSuccess)
{
throw new ScenariosReactStockCouldNotBeDecreased(@event.Isbn);
}
}
}