Querying Documents
Fisher offers several ways to read a document, and they differ in what they cost and what they carry.
| Approach | |
|---|---|
| Loading by id | LoadAsync, LoadManyAsync, CheckExistsAsync |
| LINQ | Query<T>() — where, ordering, paging, projections, grouping, joins |
| Raw JSON | Skip the serializer round trip entirely |
| Batched queries | Several reads back to back on one connection |
| Raw SQL | AdvancedSql — your SQL, typed results |
The three implicit filters
Whichever path you take, Fisher adds up to three filters you did not write:
| Filter | When |
|---|---|
| Tenant | The type is MultiTenanted() |
| Soft delete | The type is SoftDeleted() and you did not ask for deleted rows |
| Hierarchy | The type is a registered base or sub-class |
Each is applied as one statement-level pass, not by wrapping each caller predicate. That distinction is worth a paragraph, because getting it wrong is a silent cross-tenant read:
WARNING
Composing an implicit filter into a per-predicate wrapper repeats it once per predicate and omits it entirely from a query with none — so Query<T>() with no Where would return every tenant's rows. Silent, and asymmetric in the way that makes it hard to spot: the tenant owning most of the data sees a correct-looking answer with extras, and a tenant with none sees somebody else's.
All three filters are statement-level passes so that no query shape can drop one. If you are extending Fisher, do not fold any of them back into a per-predicate wrapper.
Being its own pass is also what makes AnyTenant() and TenantIsOneOf(...) possible: they replace the term, which is impossible while it is welded to each predicate.
There are exactly two places Fisher goes around the filters on purpose, and both are documented where they live: MetadataForAsync and bulk insert's duplicate probe.
Seeing the SQL
var sql = session.ToSql(session.Query<User>().Where(x => x.Internal));ToSql renders parameter names, not values, so the text is readable rather than executable. It is the cheapest way to check that an implicit filter is actually present.
Waiting for projections to catch up
var results = await session.Query<Summary>()
.QueryForNonStaleData(TimeSpan.FromSeconds(5))
.ToListAsync();TIP
QueryForNonStaleData waits for the whole store, where Polecat waits for the projections feeding the queried type. Stricter rather than weaker, and it needs no type-to-shard map.
"The whole store" means every async shard the store has registered, not every shard that has recorded progress — including one that has never run. A registered shard with no progression row is behind by definition, so the wait blocks until it reports rather than treating it as absent.
WARNING
The consequence is worth knowing before you meet it: if the async daemon is not running, this waits out its timeout and throws TimeoutException. That is the honest answer — the data really is stale and nothing is going to advance it — and the message names the shards that have recorded nothing at all, so "never started" is distinguishable from "still catching up". A store with no async projections returns immediately, because there is nothing to wait for.

JasperFx provides formal support for Fisher and other Critter Stack libraries. Please check our