Event Sourcing Feature Documentation¶
Persist aggregates as immutable event streams and rebuild state through replay and snapshots.
Overview¶
Event sourcing stores aggregate changes as an ordered stream of immutable aggregate events instead of only persisting the latest state. The current aggregate state is reconstructed by replaying those events, optionally accelerated through snapshots.
In bITdevKit, event sourcing is a separate feature from classic domain events:
- classic domain events describe post-persistence business occurrences on regular aggregates
- event sourcing models the aggregate itself as a stream of versioned aggregate events
This feature also includes its own outbox path for durable post-commit event distribution.
Persisted aggregate events, snapshots, and event-sourcing outbox payloads are closely tied to the shared serializer abstractions and conventions documented in Common Serialization.
When To Use It¶
Event sourcing is useful when you need:
- a complete change history for an aggregate
- replayable state reconstruction
- explicit aggregate-version control
- projection building from event streams
- durable downstream distribution of stored aggregate events
It is usually a deliberate architectural choice rather than the default for every module.
Core Building Blocks¶
Aggregate Events¶
Aggregate events inherit from AggregateEvent, which extends the domain-event concept with aggregate versioning:
public class PersonCreated(Guid id, int version, string firstName, string lastName)
: AggregateEvent(id, version)
{
public string FirstName { get; } = firstName;
public string LastName { get; } = lastName;
}
Each event belongs to exactly one aggregate instance and one aggregate version.
Event-Sourced Aggregate Root¶
Event-sourced aggregates inherit from EventSourcingAggregateRoot. They:
- receive new events through domain methods
- keep uncommitted events in
UnsavedEvents - replay historic events to rebuild state
- enforce sequential versioning while integrating events
The aggregate state is changed by applying events, not by directly mutating persistence-facing properties first.
Event Store¶
IEventStore<TAggregate> persists and reloads the event stream for one aggregate type. The default implementation writes events through infrastructure repositories and can rebuild an aggregate by replaying its stored stream.
Typical responsibilities are:
- append unsaved aggregate events
- load all events for one aggregate id
- rebuild the aggregate from stored events
- optionally use snapshots to avoid full replay every time
Registration and Immutable Names¶
Stored events need stable names so previously persisted data can still be resolved even when CLR type names move. The event-sourcing registration services and ImmutableNameAttribute support that mapping between persisted names and runtime types.
Setup¶
The feature spans Domain.EventSourcing plus infrastructure packages that provide storage and registration.
1. Register the event store¶
At the lower level, the EF-backed store can be registered directly:
services.AddEfCoreEventStore<MyEventStoreDbContext>(
defaultSchema: "dbo",
eventStorePublishingModes: EventStorePublishingModes.AddToOutbox);
That registration wires the core event-store services, aggregate-event repositories, snapshot support, and the EF-backed event-store outbox infrastructure.
In real SQL Server hosts, you will usually use the higher-level convenience registration instead:
services.AddEventStoreContextSqlServer<MyEventStoreDbContext>(
connectionString,
nameOfMigrationsAssembly,
defaultSchema: "dbo",
eventStorePublishingModes: EventStorePublishingModes.AddToOutbox,
maxRetryCount: 0,
maxRetryDelaySeconds: 0);
2. Register aggregate-specific access¶
services.RegisterAggregateAndProjectionRequestForEventStore<MyAggregate>(
projectionRequestPublishingModes: EventStorePublishingModes.None,
isSnapshotEnabled: true);
This makes IEventStore<MyAggregate> available and configures snapshot behavior for that aggregate type.
3. Use an event-store DbContext¶
The EF-backed store expects a DbContext derived from EventStoreDbContext, which already defines tables for:
- aggregate events
- snapshots
- the event-sourcing outbox
Aggregate Workflow¶
The typical event-sourced flow looks like this:
- Load an aggregate by replaying its stored event stream, or from a snapshot plus newer events.
- Execute a domain method that emits one or more new aggregate events.
- Save the aggregate through
IEventStore<TAggregate>. - Append the unsaved events to the event store.
- Trigger any configured downstream publication or outbox handling.
That means the event stream is the source of truth and projections or read models are downstream consumers.
Outbox Support¶
Event sourcing has its own outbox path, separate from the classic RepositoryOutboxDomainEventBehavior used by regular repositories.
When EventStorePublishingModes.AddToOutbox is enabled:
- persisted aggregate events are also written to the event-store outbox
- a worker can later read those outbox messages
- downstream projection or integration processing can happen from durable stored messages instead of only from immediate in-process dispatch
Register the worker with:
services.AddEfOutboxWorker();
This worker processes unhandled event-store outbox messages and marks them as processed once the configured downstream actions succeed.
Snapshots¶
Snapshots are an optimization, not the primary source of truth. The canonical history remains the aggregate event stream.
Enable snapshots when:
- aggregate streams grow large
- replay cost becomes noticeable
- you still want deterministic rebuilds when a full replay is necessary
If snapshots are disabled, the aggregate is rebuilt from the full stream every time it is loaded.
Relationship To Other Features¶
- Domain covers regular aggregates, value objects, typed ids, and fluent change patterns.
- Domain Events covers domain events for regular aggregates and the repository-based domain-event outbox.
- Requester and Notifier documents the general in-process dispatch infrastructure used elsewhere in the kit.