Skip to content
Document DB v7.1: Temporal Support, Telemetry Collection, & Orleans Storage Providers! Feed The Machine Here

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.

Temporal history is implemented on every provider — the relational stores and the document/NoSQL stores. Each persists versions to its own sidecar:

ProviderTemporalHistory 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.

History reads return DocumentVersion<T>:

PropertyDescription
IdThe document’s string Id.
VersionMonotonic version number, starting at 1.
ValidFromWhen this version became the current state.
ValidToWhen it was superseded — null for the version currently in effect.
OperationInserted, Updated, or Removed.
ActorThe captured actor, when CaptureActor was configured.
DocumentThe document state at this version. null for Removed tombstones.
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}");
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.

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).

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.

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.

History is unbounded by default. Configure pruning per type — both run on every write and the current version is never pruned:

OptionBehaviour
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.

  • A per-type history sidecar is created automatically (idempotently) when a temporal type is first touched. On the relational providers it’s a {table}_history table 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’s ValidTo is stamped, then a new open version is appended with the next version number.
  • For Update the 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.
  • Remove records a null-body tombstone, so AsOf correctly returns null after a deletion while the timeline stays continuous.

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.

  • Clear<T> does not record per-document history — it’s a bulk delete. Use Remove<T> per document when you need a deletion tracked.
  • History methods are on ITemporalDocumentStore, not the base IDocumentStore (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.