Skip to content

Getting Started with Reducers

Reducers provide a powerful way to build read models by reducing a sequence of events into aggregated state. This guide will walk you through creating your first reducer.

Before you begin, ensure you have:

  • A Chronicle-enabled application
  • Basic understanding of events and event sourcing
  • A read model class to reduce events into

First, create a record representing the state you want to compute:

public record OrderSummary(
Guid OrderId,
decimal TotalAmount,
int ItemCount,
DateTimeOffset LastUpdated);

Create a reducer by implementing IReducerFor<TReadModel> with methods for each event type:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reducers;
public class OrderSummaryReducer : IReducerFor<OrderSummary>
{
public OrderSummary OnOrderCreated(OrderCreated @event, OrderSummary? current, EventContext context)
{
return new OrderSummary(
OrderId: @event.OrderId,
TotalAmount: 0m,
ItemCount: 0,
LastUpdated: context.Occurred);
}
public OrderSummary OnItemAddedToOrder(ItemAddedToOrder @event, OrderSummary? current, EventContext context)
{
if (current is null) return null!; // Skip if order not created yet
return current with
{
TotalAmount = current.TotalAmount + (@event.Price * @event.Quantity),
ItemCount = current.ItemCount + @event.Quantity,
LastUpdated = context.Occurred
};
}
public OrderSummary OnItemRemovedFromOrder(ItemRemovedFromOrder @event, OrderSummary? current, EventContext context)
{
if (current is null) return null!; // Skip if order not created yet
return current with
{
TotalAmount = current.TotalAmount - (@event.Price * @event.Quantity),
ItemCount = current.ItemCount - @event.Quantity,
LastUpdated = context.Occurred
};
}
}

Reducer methods are discovered by convention and support the following signatures:

// Without context
public MyReadModel MethodName(MyEvent @event, MyReadModel? current);
// With context
public MyReadModel MethodName(MyEvent @event, MyReadModel? current, EventContext context);
// Without context
public Task<MyReadModel> MethodName(MyEvent @event, MyReadModel? current);
// With context
public Task<MyReadModel> MethodName(MyEvent @event, MyReadModel? current, EventContext context);

Key Points:

  • Method names can be anything, but typically start with On followed by the event type name
  • The @event parameter is the specific event being processed
  • The current parameter contains the existing state (null if no previous state exists)
  • The EventContext parameter provides metadata like event source ID, occurred timestamp, and sequence number
  • Both synchronous and asynchronous methods are supported

You can customize the reducer using the [Reducer] attribute:

[Reducer(id: "order-summary", eventSequence: "order-events")]
public class OrderSummaryReducer : IReducerFor<OrderSummary>
{
// Implementation
}

Attribute parameters:

  • id - Custom identifier for the reducer (defaults to the fully qualified type name)
  • eventSequence - The event sequence to observe (defaults to the event log)
  • isActive - Whether the reducer actively observes events (defaults to true)

Once your reducer is set up, you can retrieve the computed state:

public class OrderService
{
readonly IEventStore _eventStore;
public OrderService(IEventStore eventStore)
{
_eventStore = eventStore;
}
public async Task<OrderSummary?> GetOrderSummary(Guid orderId)
{
return await _eventStore.ReadModels.GetInstanceById<OrderSummary>(orderId);
}
}

Reducer methods are called for each event matching the event type:

  1. Event Type Matching - Chronicle calls the method that handles the specific event type
  2. Event Source ID - Each method receives events for a single event source
  3. Sequential Processing - Events are processed in the order they were appended

The current parameter in each method:

  • Is null when no previous state exists for this event source
  • Contains the current persisted state for this event source
  1. Keep reducers pure - Avoid side effects; only compute state from events
  2. Handle null current state - Always check if current is null for the initial state
  3. Use immutable state - Create new instances rather than mutating the current state
  4. Process events in order - The events are provided in sequence order; respect that order
  5. Consider performance - For large event streams, optimize your reduction logic