Skip to content

Storage overview

OpStream persists two things:

What Where Interface
The op log (append-only) IDocumentStore AppendOpAsync / StreamOpsAsync
Snapshots (compacted state) IDocumentStore SaveSnapshotAsync / LoadSnapshotAsync

Plus an optional history store (IHistoryStore) for the history / milestone subsystem.

Supported backends

Backend Best for Setup
In-memory Tests, demos Default
EF Core Existing EF Core code-first apps UseEfCoreStorage<TContext>()
SQL Server Microsoft-centric stacks UseSqlServer(connStr)
PostgreSQL OSS / cloud-native UsePostgreSqlStorage(...)
MySQL LAMP-style stacks UseMySqlStorage(...)
SQLite Embedded / desktop / single-process UseSqliteStorage(...)
MongoDB Document-first stacks UseMongoDbStorage(...)
Redis High-throughput, RAM-resident UseRedisStorage(...)

Use* is singleton-style — calling it twice replaces the previous registration. See Builder API conventions.

Choosing a backend

  • You already use EF CoreEF Core. Migrations land in your existing context.
  • You want maximum write throughputRedis. Sub-ms appends; persistence via AOF / RDB.
  • You want maximum simplicity for a single-server deploySQLite. One file, no infrastructure.
  • You want operational maturity in productionPostgreSQL or SQL Server.
  • Your business data is in MongoDBMongoDB keeps the op log next to it.

Implementing a custom backend

The interface is small:

public interface IDocumentStore
{
    Task AppendOpAsync(string documentId, StoredOp op, CancellationToken ct = default);
    IAsyncEnumerable<StoredOp> StreamOpsAsync(string documentId, long fromExclusive, CancellationToken ct = default);

    Task<DocumentSnapshot?> LoadSnapshotAsync(string documentId, CancellationToken ct = default);
    Task SaveSnapshotAsync(string documentId, DocumentSnapshot snapshot, CancellationToken ct = default);
}

Register your implementation with services.AddSingleton<IDocumentStore, MyStore>() after AddOpStream(), or via a custom builder extension method.

Default warnings

The default MemoryDocumentStore is registered as a fallback so AddOpStream() works out of the box. The DocumentRouter logs a warning at startup when this default is still active — that's your signal to plug in a real backend before going live.