Event Store Providers
Each event store provider implements IEventStore with database-specific optimizations. Choose the provider that matches your database.
Quick Start
Pick your database and copy the registration:
| Database | Package | Registration |
|---|---|---|
| SQL Server | Excalibur.EventSourcing.SqlServer | es.UseSqlServer(sql => sql.ConnectionString(connStr)) |
| PostgreSQL | Excalibur.EventSourcing.Postgres | es.UsePostgres(pg => pg.ConnectionString(connStr)) |
| MongoDB | Excalibur.EventSourcing.MongoDB | es.UseMongoDB(mg => mg.ConnectionString(connStr).DatabaseName("events")) |
| Cosmos DB | Excalibur.EventSourcing.CosmosDb | es.UseCosmosDb(c => c.ConnectionString(connStr).DatabaseName("events")) |
| DynamoDB | Excalibur.EventSourcing.DynamoDb | es.UseDynamoDb(opts => { ... }) |
| Firestore | Excalibur.EventSourcing.Firestore | es.UseFirestore(opts => { ... }) |
| In-Memory | Excalibur.EventSourcing.InMemory | es.UseInMemory() (builder only) |
Each AddXxxEventSourcing() call registers IEventStore and ISnapshotStore for that provider. Outbox is registered separately via services.AddExcalibur(x => x.AddOutbox(...)).
Before You Start
- .NET 10.0
- Install the provider package for your database (see below)
- Familiarity with event sourcing concepts and event store setup
SQL Server
The primary event store provider with full transaction support.
Installation
dotnet add package Excalibur.EventSourcing.SqlServer
Setup
using Microsoft.Extensions.DependencyInjection;
// Recommended: Builder-integrated registration
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseSqlServer(sql => sql.ConnectionString(connectionString))
.AddRepository<OrderAggregate, Guid>();
}));
// Or with detailed options
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseSqlServer(sql =>
{
sql.ConnectionString(connectionString)
.EventStoreSchema("es")
.SnapshotStoreSchema("es");
});
}));
// Individual stores
services.AddSqlServerEventStore(opts => opts.ConnectionString = connectionString);
services.AddSqlServerSnapshotStore(opts => opts.ConnectionString = connectionString);
// With connection factory
services.AddSqlServerEventStore(() => new SqlConnection(connectionString));
services.AddSqlServerSnapshotStore(() => new SqlConnection(connectionString));
// With typed IDb marker (multi-database scenarios)
services.AddSqlServerEventStore<IOrderDb>();
services.AddSqlServerSnapshotStore<IOrderDb>();
services.AddSqlServerEventSourcing<IOrderDb>(); // registers event store + snapshots
// Outbox is registered separately via the unified outbox package
services.AddExcalibur(excalibur => excalibur.AddOutbox(outbox => outbox.UseSqlServer(connectionString)));
PostgreSQL
Open-source alternative with Npgsql-based access.
Installation
dotnet add package Excalibur.EventSourcing.Postgres
Setup
// Recommended: Fluent builder registration
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UsePostgres(pg => pg.ConnectionString(connectionString))
.AddRepository<OrderAggregate, Guid>();
}));
// With schema and table customization
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UsePostgres(pg =>
{
pg.ConnectionString(connectionString)
.EventStoreSchema("events")
.EventStoreTable("domain_events")
.SnapshotStoreSchema("events")
.SnapshotStoreTable("snapshots");
});
}));
// With NpgsqlDataSource (recommended for connection pooling, Azure, JSONB)
var dataSource = NpgsqlDataSource.Create(configuration.GetConnectionString("Postgres")!);
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UsePostgres(pg => pg.DataSource(dataSource))
.AddRepository<OrderAggregate, Guid>();
}));
// Named connection string (resolved from IConfiguration)
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UsePostgres(pg => pg.ConnectionStringName("EventStore"));
}));
The Postgres builder supports 5 connection methods (last-wins if multiple are called):
// 1. Direct connection string (creates NpgsqlDataSource internally)
pg.ConnectionString(connectionString);
// 2. Named connection string (resolved from IConfiguration)
pg.ConnectionStringName("EventStore");
// 3. Bind from appsettings.json section
pg.BindConfiguration("EventSourcing:Postgres");
// 4. Pre-configured NpgsqlDataSource (Azure Managed Identity, JSONB, custom pooling)
pg.DataSource(preBuiltDataSource);
// 5. DataSource factory (receives IServiceProvider for DI-aware creation)
pg.DataSourceFactory(sp =>
{
var builder = new NpgsqlDataSourceBuilder(connStr);
builder.EnableDynamicJson();
return builder.Build();
});
All connection paths converge to NpgsqlDataSource for proper connection pooling — even ConnectionString and ConnectionStringName create an NpgsqlDataSource internally.
Projection Store
Register a Postgres-backed projection store for read models:
// With connection string
services.AddPostgresProjectionStore<OrderSummaryProjection>(options =>
{
options.ConnectionString = connectionString;
options.TableName = "order_summaries"; // Optional: defaults to snake_case type name
});
// With NpgsqlDataSource (recommended for connection pooling)
services.AddPostgresProjectionStore<OrderSummaryProjection>(
dataSourceFactory: sp => sp.GetRequiredService<NpgsqlDataSource>(),
configureOptions: options =>
{
options.TableName = "order_summaries";
});
PostgresProjectionStoreOptions properties:
| Property | Type | Default | Description |
|---|---|---|---|
ConnectionString | string? | Required | Postgres connection string |
TableName | string? | Type name (snake_case) | Table name for projections |
JsonSerializerOptions | JsonSerializerOptions? | camelCase, no indent | JSON serializer options for projection data |
CockroachDB and YugabyteDB Compatibility
The Postgres provider works with CockroachDB and YugabyteDB out of the box -- both databases are PostgreSQL wire-compatible and work with Npgsql. No code changes or additional packages are needed.
// CockroachDB
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UsePostgres(pg =>
pg.ConnectionString("Host=cockroachdb.example.com;Port=26257;Database=events;..."));
}));
// YugabyteDB
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UsePostgres(pg =>
pg.ConnectionString("Host=yugabyte.example.com;Port=5433;Database=events;..."));
}));
Known considerations:
| Database | Default Port | Notes |
|---|---|---|
| PostgreSQL | 5432 | Full feature support |
| CockroachDB | 26257 | Distributed SQL. SERIALIZABLE isolation by default (stricter than Postgres READ COMMITTED). |
| YugabyteDB | 5433 | Distributed SQL. Compatible with Postgres extensions. Supports NpgsqlDataSource pooling. |
All three use the same Excalibur.EventSourcing.Postgres package, DDL, and query paths. Tenant sharding (UsePostgresTenantEventStore) and parallel catch-up (PostgresRangeQueryEventStore) also work with wire-compatible databases.
For CockroachDB, set options.SchemaName = "public" (CockroachDB does not support custom schemas in the same way as PostgreSQL). For YugabyteDB, the default public schema works as expected.
Azure Cosmos DB
Globally distributed event store with partition-based scaling.
Installation
dotnet add package Excalibur.EventSourcing.CosmosDb
Setup
// Recommended: Fluent builder registration (5 canonical connection overloads)
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseCosmosDb(cosmos =>
{
cosmos.ConnectionString(connectionString)
.DatabaseName("events")
.ContainerName("event-store");
})
.AddRepository<OrderAggregate, Guid>();
}));
// With endpoint + auth key (Azure portal credentials)
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseCosmosDb(cosmos =>
cosmos.Endpoint("https://myaccount.documents.azure.com:443/", authKey)
.DatabaseName("events"));
}));
// With pre-configured CosmosClient
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseCosmosDb(cosmos =>
cosmos.Client(cosmosClient).DatabaseName("events"));
}));
The CosmosDb builder supports 5 connection methods (last-wins if multiple are called):
// 1. Connection string
cosmos.ConnectionString(connectionString);
// 2. Endpoint + auth key (Azure portal)
cosmos.Endpoint("https://myaccount.documents.azure.com:443/", authKey);
// 3. Pre-configured CosmosClient instance
cosmos.Client(existingCosmosClient);
// 4. DI-aware client factory
cosmos.ClientFactory(sp => sp.GetRequiredService<CosmosClient>());
// 5. Bind from appsettings.json section
cosmos.BindConfiguration("EventSourcing:CosmosDb");
CosmosClient is registered as a singleton — it's thread-safe and expensive to create.
Partition Strategy
Cosmos DB event stores partition by aggregate ID. Each aggregate's events are stored in a single logical partition for transactional consistency.
Amazon DynamoDB
Serverless event store for AWS workloads.
Installation
dotnet add package Excalibur.EventSourcing.DynamoDb
Setup
// Recommended: Builder-integrated registration
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseDynamoDb(options =>
{
options.TableName = "event-store";
options.Region = "us-east-1";
})
.AddRepository<OrderAggregate, Guid>();
}));
// Or with IConfiguration binding
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseDynamoDb(configuration.GetSection("DynamoDb"));
}));
// Alternative: Direct registration
services.AddDynamoDbEventStore(options =>
{
options.EventsTableName = "Events";
});
Key Schema
DynamoDB event stores use the aggregate ID as the partition key and event version as the sort key, providing efficient sequential reads per aggregate.
Google Firestore
Real-time event store for Google Cloud workloads.
Installation
dotnet add package Excalibur.EventSourcing.Firestore
Setup
// Recommended: Builder-integrated registration
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseFirestore(options =>
{
options.ProjectId = "my-gcp-project";
options.CollectionName = "events";
})
.AddRepository<OrderAggregate, Guid>();
}));
// Or with IConfiguration binding
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseFirestore(configuration.GetSection("Firestore"));
}));
// Alternative: Direct registration
services.AddFirestoreEventStore(options =>
{
options.ProjectId = "my-gcp-project";
options.EventsCollectionName = "events";
});
Collection Structure
Firestore event stores use subcollections under aggregate documents, leveraging Firestore's hierarchical document model.
MongoDB
Document-oriented event store with flexible schema and horizontal scaling via sharding.
Installation
dotnet add package Excalibur.EventSourcing.MongoDB
Setup
// Recommended: Fluent builder registration (4 canonical connection overloads)
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseMongoDB(mg =>
{
mg.ConnectionString("mongodb://localhost:27017")
.DatabaseName("events")
.CollectionName("event_store_events");
})
.AddRepository<OrderAggregate, Guid>();
}));
// With pre-configured IMongoClient
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseMongoDB(mg => mg.Client(mongoClient).DatabaseName("events"))
.AddRepository<OrderAggregate, Guid>();
}));
// With DI-aware client factory
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseMongoDB(mg =>
mg.ClientFactory(sp => sp.GetRequiredService<IMongoClient>())
.DatabaseName("events"));
}));
The MongoDB builder supports 4 connection methods (last-wins if multiple are called):
// 1. Connection string (creates IMongoClient singleton internally)
mg.ConnectionString("mongodb://localhost:27017");
// 2. Pre-configured IMongoClient instance
mg.Client(existingMongoClient);
// 3. DI-aware client factory
mg.ClientFactory(sp => sp.GetRequiredService<IMongoClient>());
// 4. Bind from appsettings.json section
mg.BindConfiguration("EventSourcing:MongoDB");
IMongoClient is registered as a singleton — it's thread-safe and expensive to create.
Document Model
MongoDB event stores use a single collection per aggregate type with the aggregate ID as the document key. Events are stored as embedded arrays within the aggregate document.
SQLite (Local Development)
Zero-Docker local development and testing. Auto-creates tables on first use.
Installation
dotnet add package Excalibur.EventSourcing.Sqlite
Setup
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseSqlite(options =>
{
options.ConnectionString = "Data Source=events.db";
});
}));
Registers both IEventStore and ISnapshotStore backed by SQLite.
| Option | Default | Description |
|---|---|---|
ConnectionString | Required | SQLite connection string (e.g., Data Source=events.db) |
EventStoreTable | "Events" | Table name for events |
SnapshotStoreTable | "Snapshots" | Table name for snapshots |
SQLite is ideal for local development, quick prototyping, and unit/integration tests where you want a real database without Docker. For production workloads, use SQL Server, PostgreSQL, or a cloud provider.
In-Memory (Testing)
For unit and integration tests:
// Recommended: Builder-integrated registration
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(es =>
{
es.UseInMemory()
.AddRepository<OrderAggregate, Guid>();
}));
// Alternative: Direct registration
services.AddInMemoryEventStore();
Provider Comparison
| Provider | Package | Transaction Support | Scaling Model |
|---|---|---|---|
| SQL Server | Excalibur.EventSourcing.SqlServer | Full ACID | Vertical + read replicas |
| PostgreSQL | Excalibur.EventSourcing.Postgres | Full ACID | Vertical + read replicas |
| MongoDB | Excalibur.EventSourcing.MongoDB | Document-level | Sharding |
| Cosmos DB | Excalibur.EventSourcing.CosmosDb | Partition-scoped | Global distribution |
| DynamoDB | Excalibur.EventSourcing.DynamoDb | Item-level | On-demand / provisioned |
| Firestore | Excalibur.EventSourcing.Firestore | Document-level | Automatic |
| SQLite | Excalibur.EventSourcing.Sqlite | Full ACID (single-writer) | Single process |
| In-Memory | Excalibur.EventSourcing.InMemory | None | Single process |
Batch Projection Registration
When registering multiple projections for the same provider, use the batch registrar API instead of individual AddXxxProjectionStore<T>() calls:
// SQL Server: register multiple projections sharing the same connection
services.AddSqlServerProjections(connectionString, projections =>
{
projections.Add<OrderSummary>();
projections.Add<CustomerProfile>(o => o.TableName = "CustomerViews");
});
// MongoDB
services.AddMongoDbProjections(connectionString, "MyApp", projections =>
{
projections.Add<OrderSummary>();
projections.Add<CustomerProfile>(o => o.CollectionName = "customers");
});
// CosmosDB
services.AddCosmosDbProjections(connectionString, "MyDatabase", projections =>
{
projections.Add<OrderSummary>();
});
// PostgreSQL
services.AddPostgresProjections(connectionString, projections =>
{
projections.Add<OrderSummary>();
});
// ElasticSearch
services.AddElasticSearchProjections("https://es.example.com:9200", projections =>
{
projections.Add<OrderSummary>();
});
See Data Providers for provider-specific details and naming conventions.
Cold Event Store Providers (Tiered Storage)
For hot/cold storage separation at petabyte scale, archived events are moved from the primary (hot) store to a cold store in blob/object storage. All cold store providers implement IColdEventStore (4 methods: WriteAsync, ReadAsync, ReadAsync(fromVersion), HasArchivedEventsAsync) and use a gzip-compressed JSON format.
Azure Blob Storage
dotnet add package Excalibur.EventSourcing.AzureBlob
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(builder =>
{
builder.UseAzureBlobColdEventStore(opts =>
{
opts.ConnectionString = "DefaultEndpointsProtocol=https;...";
opts.ContainerName = "event-archive";
opts.BlobPrefix = "events";
});
}));
AWS S3
dotnet add package Excalibur.EventSourcing.AwsS3
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(builder =>
{
builder.UseAwsS3ColdEventStore(opts =>
{
opts.BucketName = "my-event-archive";
opts.Region = "us-east-1";
opts.KeyPrefix = "events";
});
}));
Google Cloud Storage
dotnet add package Excalibur.EventSourcing.Gcs
services.AddExcalibur(excalibur => excalibur.AddEventSourcing(builder =>
{
builder.UseGcsColdEventStore(opts =>
{
opts.BucketName = "my-event-archive";
opts.ObjectPrefix = "events";
});
}));
Cold Store Comparison
| Provider | Package | Authentication |
|---|---|---|
| Azure Blob | Excalibur.EventSourcing.AzureBlob | Connection string or DefaultAzureCredential |
| AWS S3 | Excalibur.EventSourcing.AwsS3 | AWS SDK default credential chain |
| GCS | Excalibur.EventSourcing.Gcs | Google Application Default Credentials |
All providers store events as {prefix}/{aggregateId}/events.json.gz and support merge-on-write (read existing, append new, write back).
Archive Metrics
Meter: Excalibur.EventSourcing.Archive
| Metric | Type | Description |
|---|---|---|
excalibur.eventsourcing.archive.events_archived | Counter | Events moved to cold storage |
excalibur.eventsourcing.archive.events_deleted | Counter | Events removed from hot store |
excalibur.eventsourcing.archive.cold_reads | Counter | Read-through operations from cold |
excalibur.eventsourcing.archive.errors | Counter | Archive operation failures |
excalibur.eventsourcing.archive.duration_seconds | Histogram | Batch archive duration |
See Also
- Event Sourcing Overview -- Architecture and core abstractions
- Event Store --
IEventStoreinterface details - Snapshots -- Snapshot store configuration
- Change Data Capture -- CDC patterns and provider support