Skip to content
Document DB v12 - Improved Interceptors with Soft Delete Integration, AI protections, & Admin UI with Aspire Integration! How!?

Global Query Filters

Register a predicate that’s automatically AND-applied to every query of T — the same shape as Entity Framework Core’s HasQueryFilter. Use it for soft-delete, row-level security, or any “active only” scope that should be transparent to consumer code.

var store = new DocumentStore(new DocumentStoreOptions
{
DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db")
}
.AddQueryFilter<User>(u => !u.IsDeleted) // unnamed
.AddQueryFilter<Order>("tenant", o => o.TenantId == ctx.Current) // named
.AddQueryFilter<Order>("status", o => o.Status != "Archived"));

AddQueryFilter<T> is available on DocumentStoreOptions (SQLite, SQLCipher, PostgreSQL, CockroachDB, MySQL, MariaDB, SQL Server, Oracle, DuckDB) and on every provider options class (LiteDB, MongoDB, Cosmos DB, IndexedDB, Redis, RavenDB, Firestore, Azure Table, DynamoDB). It is also on IDocumentStoreOptions, the provider-agnostic slice — that’s how a feature like soft delete registers a filter once for every provider.

Multiple filters compose with AND. User-supplied .Where(...) predicates are AND’d on top.

PathFiltered?
Query<T>() and every terminal (ToList, ToAsyncEnumerable, Count, Any, Max/Min/Sum/Average, ExecuteUpdate, ExecuteDelete)Yes
query.NotifyOnChange() per-query change monitoringYes — only changes whose document matches the filter are emitted
Get<T>(id) / GetDiff<T>(id, ...)Yes — returns null when the stored document fails the filter
Update<T>Yes — throws “not found” when the stored document fails the filter
SetProperty<T> / RemoveProperty<T>Yes — returns false when the stored document fails the filter
Remove<T>(id)Yes — returns false (no-op) when the stored document fails the filter
Clear<T>()Yes — only matching documents are deleted
Count<T>(rawSql)Yes
Insert<T> / BatchInsert<T>No — inserts always succeed (matches EF Core)
Upsert<T>No — Upsert bypasses filters; use Get + Update if you need filter enforcement on the update path
Query<T>(rawSql) / QueryStream<T>(rawSql)No — raw SQL is yours to write (matches EF Core’s FromSqlRaw)
// Disable every filter on this query
var allUsers = await store.Query<User>().IgnoreQueryFilters().ToList();
// Disable specific named filters; others still apply
var anyTenant = await store.Query<Order>().IgnoreQueryFilters("tenant").ToList();
// Multiple names
var dump = await store.Query<Order>().IgnoreQueryFilters("tenant", "status").ToList();

Filters are translated on every query, so closures pick up the current value automatically:

var ctx = new TenantContext();
options.AddQueryFilter<Order>("tenant", o => o.TenantId == ctx.Current);
ctx.Current = "acme";
await store.Query<Order>().ToList(); // filters by acme
ctx.Current = "globex";
await store.Query<Order>().ToList(); // re-translated, filters by globex

This makes filters a natural building block for per-request multi-tenancy without rebuilding the store.

  • JsonTypeInfo<T> is required when filters are registered on SQL providers — the filter is translated by the same expression visitor as Where. Configure a JsonSerializerContext via DocumentStoreOptions.JsonSerializerOptions, or pass JsonTypeInfo<T> to call sites that take it. Without one, the first filtered call throws InvalidOperationException.
  • Insert bypasses filters by design. A user who inserts a User { IsDeleted = true } will have a row that’s immediately invisible to subsequent queries — match EF Core’s behavior. Catch this at the API layer if it matters.
  • Spatial sidecar tables are aware of single-row Remove (skipped when the filter rejects) but Clear<T>() with active filters skips the spatial clear entirely to avoid over-deleting. If you mix soft-delete with spatial indexing, prefer Update (setting the deleted flag) over Remove — or use AddSoftDelete, which never issues a delete at all, so the sidecar rows simply stay with the document.

TenantIdAccessor on DocumentStoreOptions is a built-in special case of a query filter — it’s wired into the SQL layer directly (adds a TenantId column and filters every read/write by it). Query filters are a generalisation: they translate arbitrary LINQ predicates and apply them at the query level. You can use both side-by-side; query filters cover everything tenancy does not, including soft-delete and row-level scopes inside a single tenant.