Skip to content

React to an event

Goal: when an event-sourced slice records something — a book is added, an author is registered — you want something else to happen automatically: a notification goes out, another system is told, a follow-up command runs. That’s a reactor.

This is part of Arc’s Chronicle integration. Direct database-backed Arc slices use commands, queries, and ordinary services; reactors become available when the write side records events in Chronicle.

A projection builds queryable state; a reactor acts. Where you’d reach for a projection to display data, reach for a reactor to cause an effect. IReactor is a marker interface — there’s nothing to override. Chronicle discovers supported handler signatures by the type of their first parameter. Register the integration and event types. For a returned-command reaction, use the supported Task<TCommand> signature shown in command side effects.

  1. For a side effect, call a collaborator. Inject whatever does the work and handle the event:

    public class NewArrivalsAnnouncer(INewArrivalsFeed feed) : IReactor
    {
    [OnceOnly]
    public async Task BookAdded(BookAddedToCatalog @event, EventContext context) =>
    await feed.Announce($"New on the shelf: {@event.Title}");
    }
  2. To invoke another behavior, execute a command. Prefer a returned command, or inject ICommandPipeline when you need to inspect the result. Chronicle also supports returned events directly; returning them preserves the side-effect pipeline without a manual default-log append:

    public class CatalogIndexer(ICommandPipeline commands) : IReactor
    {
    [OnceOnly]
    public async Task BookAdded(BookAddedToCatalog @event, EventContext context)
    {
    BookId bookId = Guid.Parse(context.EventSourceId.Value);
    var result = await commands.Execute(new CreateSearchIndex(bookId, @event.Title));
    if (!result.IsSuccess)
    {
    throw new SearchIndexingFailed();
    }
    }
    }

These reactor fragments reuse BookId, BookTitle, BookAddedToCatalog, CreateSearchIndex, and the named SearchIndexingFailed exception from command side effects. Place them in the same Catalog.Books namespace and supply INewArrivalsFeed for the first example. Import System, System.Threading.Tasks, Cratis.Arc.Commands, Cratis.Chronicle.Events, and Cratis.Chronicle.Reactors as needed.

Convert the context’s framework identity to BookId at the boundary; the command and event retain their domain types. Throwing on an unsuccessful command fails the reactor so Chronicle’s failure/recovery policy applies. Logging and returning normally would acknowledge the event despite the failed indexing. A manual command has no inherited HTTP actor; configure authorization deliberately. Returned-command execution explains system roles.

To record events and invoke commands in the same reaction, return a stable mixed collection. Arc appends one event batch before executing commands in order. An append failure prevents every command; a later command failure does not undo the event batch or earlier commands. Use EventForEventSourceId for explicit destinations and keep both kinds of effects safe to repeat on retry.

Direct appends to another event sequence or another store remain advanced options when a returned side effect cannot express the target. For the default log, return events unless an ordered workflow genuinely needs an append result, an observer-completion wait, or append options before it can proceed. In that case, ARCCHR0003’s ordered manual append example shows a narrowly scoped #pragma warning disable ARCCHR0003 with a reason and a matching restore. The warning is guidance, not a runtime ban or a reason to silence the rule across a project.

Manual appends do not appear in ReactorScenario<T>.Produced or its ShouldHaveProduced<T>() assertions. Test the append outcomes and ordering separately. Suppressing the warning provides neither replay protection nor a transaction across those appends: choose replay behavior, handle failures, and make retries safe.