Skip to content
Introducing AI Conversations: Natural Language Interaction for Your Apps! Learn More

Change Monitoring

Document DB exposes two complementary change-notification surfaces:

CapabilityWhen to use
IObservableDocumentStore — in-processReact to writes performed through this store instance. Drive reactive UI from your own writes. Buffered inside transactions and emitted on commit.
IChangeFeedDocumentStore — native change feedObserve writes from any writer (other processes, other connections, other store instances). Backed by the database’s own mechanism.

Both surface a DocumentChange<T> envelope describing what happened.

PropertyDescription
ChangeTypeInserted, Updated, Removed, or Cleared
IdThe affected document Id. Empty for Cleared (affects every document of the type).
DocumentPopulated for Inserted and full-document Updated paths. null for Removed, Cleared, SetProperty, and RemoveProperty (those paths do not materialize the document).

Implemented by DocumentStore (SQLite, SQLCipher, MySQL, SQL Server, PostgreSQL) and LiteDbDocumentStore. Cosmos DB, MongoDB, IndexedDB, and DuckDB do not implement it — calling NotifyOnChange on those stores throws NotSupportedException.

using var cts = new CancellationTokenSource();
_ = Task.Run(async () =>
{
await foreach (var change in store.NotifyOnChange<User>(cts.Token))
{
Console.WriteLine($"{change.ChangeType} {change.Id} {change.Document?.Name}");
}
});
await store.Insert(new User { Id = "u1", Name = "Alice", Age = 25 });
await store.Update(new User { Id = "u1", Name = "Alice", Age = 26 });
await store.Remove<User>("u1");
cts.Cancel();

NotifyOnChange<T> is a hot stream — subscribers only receive changes that occur after the await foreach starts iterating. Each subscriber gets its own underlying channel; multiple subscribers do not contend.

var observable = (IObservableDocumentStore)store;
await foreach (var change in observable.WhenDocumentChanged<Order>("ord-1", ct))
{
UpdateUi(change);
}

Cleared events are passed through (they affect every document of the type, including the one you are watching).

Every fluent query exposes .NotifyOnChange(ct). The change stream is filtered by the query’s Where predicates — only changes whose document matches every predicate are emitted. OrderBy, Paginate, and GroupBy are ignored (they affect result shape, not membership).

var pending = store.Query<Order>().Where(o => o.Status == "Pending");
await foreach (var change in pending.NotifyOnChange(ct))
{
// Only fires when an Order with Status == "Pending" is inserted or updated.
}

NotifyOnChange is not supported after Select(...) — call it on the source-typed query, or use IObservableDocumentStore.NotifyOnChange<T> and project in the consumer.

Changes performed inside RunInTransaction are buffered and emitted only after the transaction commits. A rollback discards them — no event is ever delivered:

await store.RunInTransaction(async tx =>
{
await tx.Insert(new User { Id = "u1", Name = "Alice" });
await tx.Insert(new User { Id = "u2", Name = "Bob" });
// Subscribers see nothing yet.
});
// Subscribers receive both Inserted events here, in order.

Cancel the CancellationToken passed to NotifyOnChange, or break out of the await foreach. The underlying channel is unregistered automatically when the iterator exits.

IChangeFeedDocumentStore.SubscribeChanges<T> observes the underlying data itself — including changes from other processes, other connections, and other store instances. Backed by each database’s native mechanism:

ProviderMechanism
PostgreSQLLISTEN / NOTIFY with row-level triggers (true push)
SQL ServerChange Tracking, optionally with SqlDependency query notifications for low-latency wake-ups (configure via SqlServerChangeFeedOptions)
Cosmos DBNative Change Feed API (with a lease container)

Provisioning — triggers, enabling Change Tracking — is automatic and idempotent. SQLite, LiteDB, IndexedDB, MySQL, and DuckDB throw NotSupportedException.

await using var sub = await store.SubscribeChanges<User>(async (change, ct) =>
{
Console.WriteLine($"{change.ChangeType} {change.Id}");
});
// Subscription runs until `sub` is disposed.

By default the SQL Server change feed polls Change Tracking on a configurable interval. Enable SqlDependency-driven query notifications for push-style wake-ups:

var store = new DocumentStore(new DocumentStoreOptions
{
DatabaseProvider = new SqlServerDatabaseProvider(
connectionString,
changeFeedOptions: new SqlServerChangeFeedOptions
{
UseQueryNotifications = true,
PollingInterval = TimeSpan.FromSeconds(15) // fallback when notifications are unavailable
})
});

The Cosmos DB change feed requires a lease container; the provider creates one automatically inside the configured database when needed.

In-process (NotifyOnChange)Native (SubscribeChanges)
SeesWrites through this store instanceAll writers to the database
Backed byIn-memory Channel<T> fan-outDatabase-native (LISTEN/NOTIFY, CT, Change Feed)
Setup costZeroProvisions triggers / change tracking lazily
Stops onCancel token / break the await foreachDispose the IAsyncDisposable handle
Buffering inside RunInTransactionYes — emitted on commitN/A (server-side mechanism)
Per-query filteringYes (query.NotifyOnChange())No — handler runs on every change
Supported providersSQLite, SQLCipher, MySQL, SQL Server, PostgreSQL, LiteDBPostgreSQL, SQL Server, Cosmos DB