CHR0022: Reactor methods returning event side effects must be marked with [OnceOnly]
Rule Description
Section titled “Rule Description”A reactor handler method that returns event(s) as a side effect causes Chronicle to append those events to the event log. Unless the method is marked with [OnceOnly], it runs again during replay operations — appending duplicate events.
Mark such a method with [OnceOnly].
Severity
Section titled “Severity”Warning
Example
Section titled “Example”Violation
Section titled “Violation”using Cratis.Chronicle.Events;using Cratis.Chronicle.Reactors;
[EventType]public record Chr0022OrderPlaced(Guid OrderId, decimal Total);
[EventType]public record Chr0022InvoiceRaised(Guid OrderId, decimal Total);
public class Chr0022Invoicing : IReactor{ // Warning CHR0022: this method returns a side-effect event, so Chronicle appends Chr0022InvoiceRaised // whenever Chr0022OrderPlaced is observed — including during replay, which would append it again. public Chr0022InvoiceRaised OrderPlaced(Chr0022OrderPlaced @event) => new(@event.OrderId, @event.Total);}using Cratis.Chronicle.Events;using Cratis.Chronicle.Reactors;
[EventType]public record Chr0022OrderPlacedFixed(Guid OrderId, decimal Total);
[EventType]public record Chr0022InvoiceRaisedFixed(Guid OrderId, decimal Total);
public class Chr0022InvoicingFixed : IReactor{ // [OnceOnly] makes Chronicle run the handler a single time per event source and skip it during // replays, so the side-effect event is appended exactly once. [OnceOnly] public Chr0022InvoiceRaisedFixed OrderPlaced(Chr0022OrderPlacedFixed @event) => new(@event.OrderId, @event.Total);}Why This Rule Exists
Section titled “Why This Rule Exists”A reactor observes events and produces side effects. When a handler returns an event, Chronicle appends it to the event log as a follow-up fact. Reactors are also re-run during replay operations — observer rewind, redaction, and revision — so a handler that appends an event and is not idempotent would append that event a second time on every replay, producing duplicate facts and, in turn, incorrect projected state.
[OnceOnly] excludes the handler from replay; it does not mean once per event source or guarantee exactly-once side effects. Failed-partition recovery and crash redelivery can run the handler again, so it must still be idempotent. The rule targets methods that return events because replaying them can append duplicate facts. See Once-only handlers for the distinction between replay and recovery.
In C#, Chronicle also appends the returned events with idempotency keys derived from the reactor delivery, which protects those events from failed-partition recovery. The keys do not run the handler less often, so the rule still applies: replay re-runs the handler, and if the output changed since the first run, the kernel refuses the append until you bump the effect epoch or mark the handler [OnceOnly]. See Duplicate protection for returned events.
Related Rules
Section titled “Related Rules”- Reactors — reactor side effects and idempotency.