Skip to content

CHR0032: Reactor must not access storage directly

A class implementing IReactor injects a storage primitive such as IMongoCollection<T> through its constructor. Injecting a sink couples the reactor to the persistence layer and bypasses Chronicle’s read-model abstraction.

Read keyed state through an injected read model parameter or IReadModels.GetInstanceById instead.

Warning

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
using MongoDB.Driver;
// Warning CHR0032: Reactor injects storage primitive 'orders' directly; this couples the
// reactor to a sink and bypasses Chronicle's read-model abstraction. Read keyed state
// through an injected read model parameter or IReadModels.GetInstanceById instead.
public class Chr0032OrderProcessor(IMongoCollection<Chr0032Order> orders) : IReactor
{
public async Task OrderPlaced(Chr0032OrderPlaced @event, EventContext context) =>
await orders.Find(order => order.OrderNumber == @event.OrderNumber).FirstOrDefaultAsync();
}
[EventType]
public record Chr0032OrderPlaced(string OrderNumber);
public record Chr0032Order(string OrderNumber);

A reactor should express intent, not reach into the persistence layer. Injecting IMongoCollection<T> ties the reactor to a specific sink (the MongoDB collection backing a read model) and lets it read or write documents directly — outside the projection pipeline that owns that state. The reactor then depends on storage-layer details and can observe or corrupt read-model state that Chronicle is responsible for building.

When a reactor needs current state, it reads it through the read-model abstraction: declare a read-model parameter on the handler (Chronicle materializes it strongly consistent for the event being handled) or call IReadModels.GetInstanceById. The read model stays the single source of derived state, and the reactor stays decoupled from where that state is persisted.