Skip to content

The search box knows all the secrets -- try it!

Fisher is part of the Critter Stack ecosystem.

JasperFx Logo JasperFx provides formal support for Fisher and other Critter Stack libraries. Please check our Support Plans for more details.

Querying Documents

Fisher offers several ways to read a document, and they differ in what they cost and what they carry.

Approach
Loading by idLoadAsync, LoadManyAsync, CheckExistsAsync
LINQQuery<T>() — where, ordering, paging, projections, grouping, joins
Raw JSONSkip the serializer round trip entirely
Batched queriesSeveral reads back to back on one connection
Raw SQLAdvancedSql — your SQL, typed results

The three implicit filters

Whichever path you take, Fisher adds up to three filters you did not write:

FilterWhen
TenantThe type is MultiTenanted()
Soft deleteThe type is SoftDeleted() and you did not ask for deleted rows
HierarchyThe 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

cs
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

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

Released under the MIT License.