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] tells Chronicle to run the handler a single time per event source and to skip it during replays, so the side-effect event is appended exactly once. Side effects that are naturally idempotent, or that do not append events, don’t need it — which is why the rule targets specifically the methods that return events.
Related Rules
Section titled “Related Rules”- Reactors — reactor side effects and idempotency.