Aggregate state in Eventuous is fully immutable and always derived from events. Rather than letting an aggregate mutate its own fields, you define a separateDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/Eventuous/eventuous/llms.txt
Use this file to discover all available pages before exploring further.
State<T> record that knows how to rebuild itself from the event stream. Every time an event is applied — either during rehydration from the store or as a new change within a command — the current state record is replaced by a new instance returned from the appropriate handler. Nothing is ever mutated in place.
Class signature
State<T> is an abstract record, you get value equality, non-destructive mutation via with expressions, and a clean inheritance model out of the box. The where T : State<T> (CRTP) constraint allows the base class to return strongly-typed instances from its methods without unsafe casts in your code.
Registering event handlers with On<TEvent>()
On<TEvent> in the constructor of your state class to register a handler for a specific event type. The handler receives the current state instance and the event, and must return a new state instance — typically built with a with expression:
On<TEvent> twice for the same event type, a DuplicateTypeException<TEvent> is thrown immediately at startup — making misconfiguration a hard failure rather than a silent bug.
Dispatching events with When(object @event)
When is called by the aggregate (via Apply) whenever an event needs to be applied. It looks up the registered handler for @event.GetType() and invokes it. If no handler is registered for the given event type, When returns the current instance unchanged — meaning unknown event types are safely ignored during rehydration of old streams.
Introspection with GetRegisteredEventTypes()
Type objects that have registered handlers in this state class. Eventuous uses this internally for diagnostic tooling such as Eventuous Spyglass, which calls GetRegisteredEventTypes() at startup to enumerate the events each state handles. You can also call it in tests to assert that a state class covers all expected event types.
The State<T, TId> variant
State<T, TId>, the state record automatically gains a typed Id property. The aggregate infrastructure sets Id when the aggregate is loaded from a named event stream, so you never need to carry the identity through the events themselves. Use this variant when you need the aggregate identity to be available inside state queries or read-model projections that receive a state snapshot.
Full example — BookingState
TheBookingState record below handles every event that the Booking aggregate can produce. Each On<> handler uses a with expression to return a new record with only the changed properties updated.
Key design rules
- Always return a new instance. Because
State<T>is arecord, thewithexpression is the idiomatic way to produce an updated copy. Never mutate any property inside a handler. - Handlers are registered in the constructor. This is the only place to call
On<TEvent>. The handler dictionary is built once and never modified. - Unknown events are ignored. If an event type has no handler,
Whenreturnsthisunchanged. This lets old aggregates rehydrate gracefully from streams that contain events introduced by a newer version of the application. - Keep state handlers pure. Handlers receive the current state and the event; they should not call services, query databases, or produce side effects. All business decisions live in the aggregate class; state records only transform data.