Temporal Support
Temporal support records a versioned snapshot of a document on every mutation, so you can read its state as it was at any point in time, audit who changed what and when, restore a prior version, and diff between versions. History is opt-in per type and append-only: writes go to a per-type history sidecar alongside the live data.
It is the system-time (“transaction-time”) model — the database records the interval during which each version was the current truth. Enable it with MapTemporal<T>:
var options = new DocumentStoreOptions{ DatabaseProvider = new SqliteDatabaseProvider("Data Source=app.db")};
options.MapTemporal<Order>(o =>{ o.Retention = TimeSpan.FromDays(90); // prune expired versions older than this o.MaxVersions = 50; // …or cap versions kept per document o.CaptureActor = () => currentUser.Id; // optional "who" recorded on each version});Every Insert, Update, Upsert, Remove, SetProperty, RemoveProperty, and BatchInsert for Order now appends a version — including writes made inside RunInTransaction (buffered and committed atomically with the main write). Only mapped types incur the extra write; everything else is untouched.
Provider support
Section titled “Provider support”Temporal history is implemented on every provider — the relational stores and the document/NoSQL stores. Each persists versions to its own sidecar:
| Provider | Temporal | History sidecar |
|---|---|---|
| SQLite | ✅ | {table}_history table |
| SQLCipher | ✅ | {table}_history table |
| PostgreSQL | ✅ | {table}_history table |
| SQL Server | ✅ | {table}_history table |
| MySQL | ✅ | {table}_history table |
| Oracle | ✅ | {table}_history table |
| DuckDB | ✅ | {table}_history table |
| LiteDB | ✅ | {collection}_history collection |
| MongoDB | ✅ | {collection}_history collection |
| Cosmos DB | ✅ | {container}_history container (partitioned by /typeName) |
| IndexedDB | ✅ | {store}_history object store |
Why the history methods aren’t on IDocumentStore
Section titled “Why the history methods aren’t on IDocumentStore”History is an optional capability, not part of the universal CRUD contract, so it lives on its own interface — ITemporalDocumentStore : IDocumentStore — the same way observation lives on IObservableDocumentStore and the native change feed on IChangeFeedDocumentStore. Promoting History / AsOf / Restore / … to IDocumentStore would force every consumer of a plain store to see seven methods that throw far more often than they work (they require the type to be MapTemporal-mapped), and force every backend to implement them whether the concept applies or not. Asking for ITemporalDocumentStore instead makes “this store does history” a compile-time, discoverable fact. It also follows the same precedent as Backup / ClearAllAsync, which sit outside the universal interface for the same reason.
Every store implements ITemporalDocumentStore (and a temporal store is a full document store, since the interface extends IDocumentStore). Resolve or cast to it:
var store = serviceProvider.GetRequiredService<ITemporalDocumentStore>();Calling a history method for a type that wasn’t passed to MapTemporal<T> throws InvalidOperationException.
DocumentVersion<T>
Section titled “DocumentVersion<T>”History reads return DocumentVersion<T>:
| Property | Description |
|---|---|
Id | The document’s string Id. |
Version | Monotonic version number, starting at 1. |
ValidFrom | When this version became the current state. |
ValidTo | When it was superseded — null for the version currently in effect. |
Operation | Inserted, Updated, or Removed. |
Actor | The captured actor, when CaptureActor was configured. |
Document | The document state at this version. null for Removed tombstones. |
Per-document queries
Section titled “Per-document queries”History — every version of one document
Section titled “History — every version of one document”IReadOnlyList<DocumentVersion<Order>> history = await store.History<Order>(orderId);
foreach (var v in history) Console.WriteLine($"v{v.Version} {v.Operation} by {v.Actor} at {v.ValidFrom}");AsOf — state at a point in time
Section titled “AsOf — state at a point in time”Order? lastTuesday = await store.AsOf<Order>(orderId, new DateTimeOffset(2026, 6, 9, 0, 0, 0, TimeSpan.Zero));Returns null if the document did not exist (or had been removed) at that instant.
Restore — reinstate a prior version
Section titled “Restore — reinstate a prior version”Order? restored = await store.Restore<Order>(orderId, version: 7);Restore writes a new current version (it does not rewrite history): the document is re-inserted if it had been removed, otherwise overwritten with the version-7 state. Returns null if that version doesn’t exist or was a removal tombstone. When a version property is mapped, the optimistic-concurrency token is aligned to the live row so the restore isn’t rejected as stale.
GetDiffBetween — RFC 6902 patch between two versions
Section titled “GetDiffBetween — RFC 6902 patch between two versions”The temporal analogue of GetDiff:
JsonPatchDocument<Order>? patch = await store.GetDiffBetween<Order>(orderId, fromVersion: 3, toVersion: 7);Returns null if either version is missing or is a removal tombstone (no body to diff).
Fleet-wide queries
Section titled “Fleet-wide queries”These span every document of the type and are backed by secondary indexes on the history table.
AsOfAll — point-in-time snapshot of all documents
Section titled “AsOfAll — point-in-time snapshot of all documents”IReadOnlyList<Order> snapshot = await store.AsOfAll<Order>(endOfQuarter);Returns the live state of every document that existed (and was not removed) at that instant. Tombstones are excluded.
ChangesByActor — per-user audit trail
Section titled “ChangesByActor — per-user audit trail”IReadOnlyList<DocumentVersion<Order>> byAlice = await store.ChangesByActor<Order>("alice@corp.com");Every version authored by a given actor, oldest first. Requires a configured CaptureActor.
ChangesBetween — audit log over a time window
Section titled “ChangesBetween — audit log over a time window”var changes = await store.ChangesBetween<Order>(from: weekStart, to: weekEnd);Every version whose ValidFrom falls in [from, to), across all documents of the type, oldest first.
Retention
Section titled “Retention”History is unbounded by default. Configure pruning per type — both run on every write and the current version is never pruned:
| Option | Behaviour |
|---|---|
Retention (TimeSpan?) | Deletes closed (expired) versions whose ValidTo is older than now - Retention. |
MaxVersions (int?) | Keeps only the newest N versions per document. |
options.MapTemporal<Order>(o =>{ o.Retention = TimeSpan.FromDays(90); o.MaxVersions = 50;});On mobile/embedded SQLite especially, set at least one — unbounded history grows the database file with every write.
How it works
Section titled “How it works”- A per-type history sidecar is created automatically (idempotently) when a temporal type is first touched. On the relational providers it’s a
{table}_historytable with primary key(Id, TypeName, Version)plus secondary indexes on(TypeName, ValidFrom, ValidTo)and(TypeName, Actor)to back the fleet-wide queries; the document stores hold the same versions in a native sidecar collection / container / object store and compute the point-in-time selection in the provider. - Each version row carries
Id,Version,ValidFrom/ValidTo,Operation,Actor, and the post-image. On each mutation the currently-open version’sValidTois stamped, then a new open version is appended with the next version number. - For
Updatethe full post-image is recorded directly. For merge/partial paths (Upsert,SetProperty,RemoveProperty) the resulting document is read back so history always stores the true post-image — this read-back cost is incurred only for temporal-mapped types. Removerecords a null-body tombstone, soAsOfcorrectly returnsnullafter a deletion while the timeline stays continuous.
IndexedDB: bump the database version
Section titled “IndexedDB: bump the database version”Because temporal adds new object stores, IndexedDB only creates them during a schema upgrade. A fresh database picks them up automatically; for an already-deployed one, increment options.Version when you add MapTemporal so the upgrade runs and creates the {store}_history object stores.
Limitations
Section titled “Limitations”Clear<T>does not record per-document history — it’s a bulk delete. UseRemove<T>per document when you need a deletion tracked.- History methods are on
ITemporalDocumentStore, not the baseIDocumentStore(see above). - Documents written before a type was made temporal have no prior history; their first subsequent update starts the timeline at version 1.
Like every other API, the history methods accept an optional JsonTypeInfo<T> for source-generated serialization, and otherwise resolve type info from the configured JsonSerializerContext. See AOT Setup.