Documentation 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.
Eventuous separates persistence concerns into focused interfaces: IEventReader for reading events out of streams, IEventWriter for appending new events, and IEventStore for the full lifecycle including stream management. Every concrete event store adapter — KurrentDB, PostgreSQL, MongoDB — implements these interfaces, so your domain and application code stays independent of the underlying storage engine.
Interface hierarchy
IEventStore extends both IEventReader and IEventWriter, making it the single type to register when you want full read-write-manage access:
public interface IEventStore : IEventReader, IEventWriter
{
Task<bool> StreamExists(StreamName stream, CancellationToken cancellationToken = default);
Task TruncateStream(StreamName stream, StreamTruncatePosition truncatePosition,
ExpectedStreamVersion expectedVersion, CancellationToken cancellationToken = default);
Task DeleteStream(StreamName stream, ExpectedStreamVersion expectedVersion,
CancellationToken cancellationToken = default);
}
You can also depend on IEventReader or IEventWriter in isolation when a component only needs one capability (e.g., a read model rebuilder needs only IEventReader).
Reading events
IEventReader exposes two streaming methods that return an IAsyncEnumerable<StreamEvent>:
// Read events forward from a given position
IAsyncEnumerable<StreamEvent> ReadEvents(
StreamName stream,
StreamReadPosition start,
int count,
CancellationToken cancellationToken);
// Read events backward from a given position
IAsyncEnumerable<StreamEvent> ReadEventsBackwards(
StreamName stream,
StreamReadPosition start,
int count,
CancellationToken cancellationToken);
StreamReadPosition wraps a long revision number. Two well-known sentinels are available:
| Sentinel | Meaning |
|---|
StreamReadPosition.Start | Begin reading from revision 0 |
StreamReadPosition.End | Seek to the last event |
Both methods throw StreamNotFound if the stream does not exist.
Helper extensions
The static class StoreFunctions adds higher-level helpers on top of IEventReader:
// Read all events in a stream, paged automatically (500 events per page)
Task<StreamEvent[]> ReadStream(
this IEventReader eventReader,
StreamName streamName,
StreamReadPosition start,
bool failIfNotFound = true,
CancellationToken cancellationToken = default);
// Read a fixed window into an array (forward or backward)
Task<StreamEvent[]> ReadEvents(
this IEventReader eventReader,
StreamName stream,
StreamReadPosition start,
int count,
bool failIfNotFound,
CancellationToken cancellationToken);
Setting failIfNotFound to false returns an empty array instead of throwing when the stream is absent.
Loading aggregates
AggregatePersistenceExtensions adds LoadAggregate directly on IEventReader:
// Load by explicit stream name
Task<TAggregate> LoadAggregate<TAggregate, TState>(
StreamName streamName,
bool failIfNotFound = true,
AggregateFactoryRegistry? factoryRegistry = null,
CancellationToken cancellationToken = default)
where TAggregate : Aggregate<TState>
where TState : State<TState>, new();
// Load by aggregate identity (resolves stream name automatically)
Task<TAggregate> LoadAggregate<TAggregate, TState, TId>(
TId aggregateId,
StreamNameMap? streamNameMap = null,
bool failIfNotFound = true,
AggregateFactoryRegistry? factoryRegistry = null,
CancellationToken cancellationToken = default)
where TAggregate : Aggregate<TState>
where TState : State<TState>, new()
where TId : Id;
Writing events
IEventWriter has one core method and a default multi-stream overload:
// Append events to a single stream
Task<AppendEventsResult> AppendEvents(
StreamName stream,
ExpectedStreamVersion expectedVersion,
IReadOnlyCollection<NewStreamEvent> events,
CancellationToken cancellationToken);
// Append to multiple streams (stores that support atomic multi-stream writes override this)
Task<AppendEventsResult[]> AppendEvents(
IReadOnlyCollection<NewStreamAppend> appends,
CancellationToken cancellationToken);
Storing aggregates
The StoreAggregate extension on IEventWriter serialises an aggregate’s pending changes and persists them:
// Store by stream name
await writer.StoreAggregate<Booking, BookingState>(
streamName, booking, amendEvent, cancellationToken);
// Store by aggregate identity (stream name resolved via StreamNameMap)
await writer.StoreAggregate<Booking, BookingState, BookingId>(
booking, bookingId, streamNameMap, amendEvent, cancellationToken);
Event record types
NewStreamEvent
Represents an event that is about to be written:
public record struct NewStreamEvent(Guid Id, object? Payload, Metadata Metadata);
| Property | Type | Description |
|---|
Id | Guid | Unique identifier for this event instance |
Payload | object? | The domain event object |
Metadata | Metadata | Key/value bag attached to the event |
StreamEvent
Represents an event that was read back from the store:
public record struct StreamEvent(
Guid Id,
object? Payload,
Metadata Metadata,
string ContentType,
long Revision,
DateTime Created = default,
bool FromArchive = false);
Revision is the zero-based position of the event within its stream. FromArchive is set when the event was loaded via a tiered/archive reader.
AppendEventsResult
Returned from every successful append:
public record AppendEventsResult(ulong GlobalPosition, long NextExpectedVersion);
| Property | Type | Description |
|---|
GlobalPosition | ulong | Global log position of the last written event |
NextExpectedVersion | long | Stream version to use in the next conditional append |
AppendEventsResult.NoOp is returned when there are no changes to persist.
ExpectedStreamVersion
ExpectedStreamVersion enforces optimistic concurrency. Pass it to any append or truncate/delete call:
// New stream — fails if the stream already exists
ExpectedStreamVersion.NoStream // Value = -1
// Unconditional write — always succeeds regardless of current version
ExpectedStreamVersion.Any // Value = -2
// Conditional write at a known version
new ExpectedStreamVersion(42) // Must match current stream revision
If the stream’s current version does not match the expected version, the store throws an OptimisticConcurrencyException.
AmendEvent delegate
AmendEvent is a hook that lets you inject additional metadata into every NewStreamEvent before it is written to the store. This is typically used to attach correlation IDs, causation IDs, or user identity from the current request context:
// Simple amendment — no extra context
public delegate NewStreamEvent AmendEvent(NewStreamEvent originalEvent);
// Context-aware amendment
public delegate NewStreamEvent AmendEvent<in T>(NewStreamEvent originalEvent, T context);
Example — stamping a correlation ID from the HTTP context:
AmendEvent amendEvent = original => original with {
Metadata = original.Metadata.With("correlationId", correlationId)
};
Pass the delegate to StoreAggregate or Store helpers; it is applied to every event in the batch.
Dependency injection
Eventuous.Extensions.DependencyInjection provides three registration helpers. Each one automatically wires up OpenTelemetry tracing when diagnostics are enabled:
// Register a full IEventStore (also registers IEventReader and IEventWriter)
builder.Services.AddEventStore<KurrentDBEventStore>();
// Register separate reader / writer implementations
builder.Services.AddEventReader<MyEventReader>();
builder.Services.AddEventWriter<MyEventWriter>();
// Register a single class that implements both reader and writer
builder.Services.AddEventReaderWriter<MyReadWriteStore>();
AddEventReader, AddEventWriter, and AddEventReaderWriter use TryAddSingleton, so a previously registered implementation is not overwritten. AddEventStore registers IEventStore, IEventReader, and IEventWriter together; when OpenTelemetry diagnostics are enabled it uses AddSingleton for the traced wrappers, so call it only once.
// Factory overload — useful when the constructor needs runtime configuration
builder.Services.AddEventStore<KurrentDBEventStore>(
sp => new KurrentDBEventStore(sp.GetRequiredService<EventStoreClient>()));
Implementing a custom event store
Implement IEventStore to integrate any storage backend. The minimum surface is:
public class MyEventStore : IEventStore {
public IAsyncEnumerable<StreamEvent> ReadEvents(
StreamName stream, StreamReadPosition start, int count, CancellationToken ct) { /* ... */ }
public IAsyncEnumerable<StreamEvent> ReadEventsBackwards(
StreamName stream, StreamReadPosition start, int count, CancellationToken ct) { /* ... */ }
public Task<AppendEventsResult> AppendEvents(
StreamName stream, ExpectedStreamVersion expectedVersion,
IReadOnlyCollection<NewStreamEvent> events, CancellationToken ct) { /* ... */ }
public Task<bool> StreamExists(StreamName stream, CancellationToken ct) { /* ... */ }
public Task TruncateStream(StreamName stream, StreamTruncatePosition pos,
ExpectedStreamVersion ver, CancellationToken ct) { /* ... */ }
public Task DeleteStream(StreamName stream, ExpectedStreamVersion ver,
CancellationToken ct) { /* ... */ }
}