Skip to content

CHR0031: Reactor must not have mutable state

A class implementing IReactor declares a mutable instance field or settable property. Reactors observe events and produce side effects; Chronicle re-creates and replays them, so mutable instance state is unreliable and leaks context between invocations.

Keep reactors stateless: express dependencies as readonly, primary-constructor-injected fields, and derive everything else from the event and event context.

Warning

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
public class Chr0031OrderCounter : IReactor
{
// Warning CHR0031: Reactor declares mutable state '_count'; reactors are re-created and
// replayed by Chronicle, so instance state is unreliable and leaks context between
// invocations. Use readonly, primary-constructor-injected dependencies.
int _count;
public Task OrderPlaced(Chr0031OrderPlaced @event, EventContext context)
{
_count++;
return Task.CompletedTask;
}
}
[EventType]
public record Chr0031OrderPlaced(string OrderNumber);

A reactor has no reliable lifetime you can accumulate state across. Chronicle re-creates reactor instances and replays event streams through them, and a single event source may be processed more than once (recovery, rewind, redaction, revision). A mutable field or settable property therefore holds a value that depends on which events happened to run through this particular instance — a counter that is wrong after a replay, or a value that bleeds from one event source into the next.

Everything a reactor needs is already available: the collaborators it calls (injected once through the primary constructor as readonly fields) and the facts of the event it is handling (the event payload and EventContext). Anything derived should be computed from those inputs each time the handler runs, not stored on the instance. static const, static readonly, readonly, and init-only members are fine — the rule flags only mutable non-static fields and properties with a non-init setter.