Skip to content

Reducers

A Chronicle reducer is an observer that folds events into read-model state using code you write.

When a view depends on the previous state, writing its transitions as code can be clearer than mapping properties. A reducer folds events one at a time, receiving the state the previous event produced and returning the next state for that event source.

Start with model-bound projection attributes, use fluent mappings when the definition needs structure, and choose a reducer when state-transition code is clearer; a self-reducing model is the default for new reducer code. Choose how to build a read model compares the forms and shows when a separate instance reducer is useful.

A reducer is a specialized observer that:

  • Processes events one at a time, in order - Chronicle may deliver several events for an event source in one batch, but it calls your reducer method once per event, passing the state the previous event produced
  • Computes derived state - The reducer method receives the event and the current state, and returns the new state
  • Maintains temporal consistency - All events for a given event source are processed in order
  • Maintains materialized state - Read the currently stored state; historical reducer snapshots are not provided by GetSnapshotsById

Reducers are ideal when you need to:

  • Aggregate data across multiple events - Calculate sums, averages, or other metrics from a series of events
  • Build temporal models - Track how state changes over time
  • Express state-dependent logic - Branch on previous state or calculate values with guards
  • Reconstruct state - Fold a history on demand or rebuild materialized state during replay
using Cratis.Chronicle.Events;
[EventType]
public record SelfBalanceAdded(decimal Amount);
[EventType]
public record SelfBalanceCleared;
public record SelfAccountBalance(decimal Balance)
{
public static SelfAccountBalance Reduce(SelfBalanceAdded @event, SelfAccountBalance? current) =>
new((current?.Balance ?? 0m) + @event.Amount);
public static SelfAccountBalance? Reduce(SelfBalanceCleared @event, SelfAccountBalance? current) => null;
}

View C# snippet source on GitHub

The static Reduce shape lets Chronicle discover the model’s fold without a marker or separate class. Continue with self-reducing read models to register, test, and configure it.

Use IReducerFor<T> when the fold needs injected services, instance behavior during replay, or must live apart from a shared model contract. See the separate-reducer guide.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reducers;
[EventType]
public record ReducersIndexDepositMade(decimal Amount);
[EventType]
public record ReducersIndexWithdrawalMade(decimal Amount);
public record ReducersIndexAccountBalance(decimal Balance, DateTimeOffset LastUpdated);
public class ReducersIndexAccountBalanceReducer : IReducerFor<ReducersIndexAccountBalance>
{
public ReducersIndexAccountBalance Deposited(ReducersIndexDepositMade @event, ReducersIndexAccountBalance? current, EventContext context)
{
var currentBalance = current?.Balance ?? 0m;
return new ReducersIndexAccountBalance(currentBalance + @event.Amount, context.Occurred);
}
public ReducersIndexAccountBalance WithdrawalMade(ReducersIndexWithdrawalMade @event, ReducersIndexAccountBalance? current, EventContext context)
{
var currentBalance = current?.Balance ?? 0m;
return new ReducersIndexAccountBalance(currentBalance - @event.Amount, context.Occurred);
}
}

View C# snippet source on GitHub

In .NET, a reducer class must target only one read model. Implementing two closed IReducerFor<TReadModel> interfaces, including through inheritance, throws TypeMustImplementOnlyOneReducer during default artifact discovery. Split the class into one reducer per read model. CHR0057 reports this before runtime.

The .NET client can also explicitly nominate a plain classic reducer and infer its single read model from supported instance-method signatures. Alternatively, a concrete model with the exact public static Reduce(event, nullable current[, EventContext]) shape is discovered as a self reducer, without a marker. The existing RegisterReducer APIs accept either form; see explicit artifact registration.

Once you’ve defined a reducer, you can retrieve and observe the resulting read models using the IReadModels API: