Skip to content

State and Reducers

State is what you get when you fold a subject's events into a single value: the current title of a book, whether it is checked out, who holds it. EventSourcingKit builds state on demand by replaying events through reducers, and this is the model you use on the write side to make decisions before storing new events.

Defining State

A state type derives from State<TId>, where TId is the type of the entity's identifier:

public record BookState : State<Guid>
{
    public string Title { get; init; } = "";
    public bool IsCheckedOut { get; init; }
}

Every replayed state also exposes the Event it was last updated from, so you always know which event produced the current value. If a state spans more than one subject, derive from the non-generic State instead, which carries no identifier.

Writing Reducers

A reducer describes how one event type changes the state. The reducer interfaces live in the EventSourcingKit.Replaying namespace:

  • ICreateReducer<TState, TId, TEvent> builds the initial state from the first event, through Create.
  • IUpdateReducer<TState, TId, TEvent> evolves an existing state, through Apply.
  • IUpsertReducer<TState, TEvent> handles a state that is not keyed by a single subject, creating or updating it in one method.
public class BookRegisteredReducer
    : ICreateReducer<BookState, Guid, BookRegistered>
{
    public BookState Create(Event<BookRegistered> @event) =>
        new() { Id = @event.Data.BookId, Title = @event.Data.Title };
}

Reducers are discovered automatically from the assemblies you register, the same way events are, so you never register them individually.

Replaying

Two methods turn events into state, each available for a subject or an EventQL query:

  • ReplayToCurrentState<TState, TId> returns the single, latest state. This is the common case.
  • Replay<TState, TId> streams the state after every event, which is useful for auditing how a value evolved over time.
var book = await eventStore.ReplayToCurrentState<BookState, Guid>(
    subject,
    cancellationToken
);

The natural use is to load an entity, check an invariant, and write: replay the state, confirm the action is allowed, then store the new event guarded by a precondition.

State versus Projections

State is built on demand and is ideal for write-side decisions. When you instead need a continuously maintained, queryable read model, use a projection, which applies the same reducer idea in the background.

For More Information