Returning commands as side effects
Goal: a reactor sees an event and needs to change state in response — index a book, archive a record, notify a downstream slice. The usual way is to inject ICommandPipeline and call Execute yourself. But when all a reaction does is fire one or more commands, that plumbing is noise. Instead, return the command from the handler method and Arc runs it for you.
Return a command
Section titled “Return a command”A reactor method dispatches on the type of its first parameter, and its return value becomes the side effect. Return a [Command] and Arc executes it:
[Command]public record CreateSearchIndex(BookId BookId, string Title){ public Task Handle(/* dependencies */) => /* ... */;}
public class CatalogIndexer : IReactor{ public CreateSearchIndex BookAdded(BookAddedToCatalog @event) => new(@event.BookId, @event.Title);}That is the whole reactor. No ICommandPipeline, no Execute call. Under the hood Arc detects that the returned value is a command, creates a dedicated service scope, and runs it through the pipeline — validation, authorization, and Handle() — exactly as if it had been sent from an HTTP endpoint.
The method can also be asynchronous — return Task<CreateSearchIndex> — if you need to compute the command with await.
Return several commands
Section titled “Return several commands”When one event should trigger a handful of commands, return them as a collection:
public class BookArchiver : IReactor{ public IEnumerable<object> BookRemoved(BookRemovedFromCatalog @event) => [ new ArchiveBookMetadata(@event.BookId), new RemoveFromSearchIndex(@event.BookId), new NotifySubscribers(@event.BookId) ];}The commands run sequentially, in order, within a single service scope. If one fails, execution stops there — the commands after it do not run.
When a command fails
Section titled “When a command fails”If a returned command comes back unsuccessful — a validation error, an authorization failure, or an exception thrown from its Handle() — the reactor fails for that event. Chronicle then pauses the event source partition for that reactor until the problem is resolved and processing is retried, the same failure behavior as any other reactor that throws.
When you need finer control
Section titled “When you need finer control”Returning commands is deliberately simple: run them, fail the reactor if any fails. When you need to decide what happens on failure — log and carry on, run a compensating command, branch on the result — inject ICommandPipeline and execute the command yourself so you can inspect the CommandResult:
public class ResilientIndexer(ICommandPipeline commands, ILogger<ResilientIndexer> logger) : IReactor{ public async Task BookAdded(BookAddedToCatalog @event) { var result = await commands.Execute(new IndexBookForSearch(@event.BookId, @event.Title)); if (!result.IsSuccess) { logger.LogWarning("Could not index book {BookId}", @event.BookId); } }}A single handler method does one or the other — either return commands, or execute them manually and return Task. It can’t do both in the same invocation.
See also
Section titled “See also”- React to an event — the fundamentals of reactors and when to reach for one.
- Commands — how Arc commands are defined and executed.