Events, state, and snapshots
An event records an accepted domain fact. The event stream is the actor's write history; its current in-memory state is rebuilt by applying that history. A read model is another view over relevant facts, optimized for the reader that uses it.
On this page
In the workshop example, RegistrationRequested records the workshop, attendee, and display name. RegistrationCancelled records the cancelled result. Applying those events produces the current registration state.
Keep event application pure
[GenerateSerializer]
public record RegistrationState(Guid RegistrationId, Guid WorkshopId,
Guid AttendeeId, string DisplayName, string Status) : IActorState
{
public static RegistrationState Empty => new(Guid.Empty, Guid.Empty,
Guid.Empty, "", "absent");
public RegistrationState Apply(RegistrationRequested e) =>
new(e.RegistrationId, e.WorkshopId, e.AttendeeId, e.DisplayName, "requested");
public RegistrationState Apply(RegistrationCancelled e) =>
new(e.RegistrationId, e.WorkshopId, e.AttendeeId, e.DisplayName, "cancelled");
}
These methods return a new state record. They do not send commands, query SQL, or call a provider. The actor can apply historical events during activation and recover the same state without repeating outside work.
The command handler has another job: validate intent against current state and decide events. The projection and orchestration listeners handle their configured event delivery separately. Replay and live events shows that separation visually.
Message and source identity
Event carries Metadata, including correlation, causation, user, device, message, and transaction IDs. Those fields explain attribution and relationships. They do not alone establish current authorization or projection deduplication.
EventEnvelope<T> adds a source locator:
| Field | Meaning |
|---|---|
Id |
The source actor's key. It is not a per-event UUID. |
Stream |
The source stream name. |
Version |
The source position under the producer/provider's version contract. |
Event |
The typed domain event and its metadata. |
A listener can combine stream and version with its consumer identity to recognize a delivered source. Filtered events and framework records can affect which version values a particular consumer sees, so do not assume every observed number is consecutive.
For the full explanation of the six header fields and current dispatch behavior, read message metadata and correlation and causation.
Typed state and creation
EventSourcedActor<TState> and StatefulActor<TState> provide typed state shapes. CreatableActor adds an explicit creation lifecycle for actors that use a create command. An actor reference can exist before the corresponding domain entity exists; define what each command does in that state.
Use stable actor IDs and reject conflicting reuse. A cancellation or retirement may also need durable lifecycle evidence so delayed work cannot recreate the entity accidentally. Actor boundaries and idempotency explore those choices.
Snapshots
Snapshots can reduce replay work for a compatible actor/store path. They must remain tied to committed event history and the state schema. They are not a replacement for event history or a signal that all external projections are current. Authority-sensitive source-observation paths can bypass legacy snapshots.
The generic event-sourced actor reads its configured snapshot policy; a serving host must still choose appropriate retention, serializer compatibility, and recovery behavior. A snapshot should not contain prospective uncommitted state or become an independent source of business authority.
Evolve history deliberately
Changing a C# record does not rewrite stored events. Keep historical event types and payloads readable, version the persisted format where needed, and replay representative histories before introducing new writers.
Records provide useful value semantics, but mutable nested lists or arrays can still change after construction. Use immutable collections or defensively copy retained payloads, particularly before asynchronous persistence.
Continue with stores and serialization, read-model design, and the workshop walkthrough.