Limitations
These are Sluice's deliberate limits, listed here so you don't discover them in production. Each links to the page that explains it. Where a limit can leave an old value cached, it lives no longer than the entry's duration.
Writes Sluice can't see
- Anything that writes without a
UseSluicecontext must invalidate by hand: another service, a cron job, SQL run by hand, a database trigger, a context withoutUseSluice, andExecuteSql. Otherwise the old value stays cached until it expires, so keep durations short for data written outside your app. See Writes Sluice can't see. - On the generic path, every write is yours to report, after it commits. Before the commit, or never, and the old value can stay until it expires. See After the commit, never before.
Everywhere
- A crash between commit and invalidation loses the invalidation. Nothing replays it after a restart. See Transactions.
- A FusionCache 2.9 race can lose an invalidation on one server. An invalidation that lands in the same microseconds as a read checking that dependency can be overwritten by the check. That server then keeps serving the values cached before it until they expire or are replaced, or the dependency is invalidated again. The window opens when a server first checks a dependency, and again about once an hour after that. It's a FusionCache bug; Sluice has no workaround.
- Computes must read committed data from the primary database, not inside your own open transaction or from a replica that may lag behind. See Computes and transactions.
- Whatever changes what a compute reads belongs in the query key: a tenant, a locale, a soft-delete switch. See Tenants.
- Cached values are shared instances, entities included, unless you turn on auto-clone. See Shared instances.
- Your
formatfunctions andIFormattableids must give different text for different keys. Sluice can't check that. See Key text. - Source names aren't checked for duplicates, and query names only within one process. See Names and Queries.
- Renaming a source or query, or changing a
format, changes its cache keys. During a rolling deploy, old and new servers don't evict each other's entries. See Renaming changes the cache keys. - Moving or renaming an EF entity class changes its dependencies' names, which are the class's full name (
Shop.Domain.Order). During a rolling deploy, old and new servers don't evict each other's cached reads of that type until those entries expire. See What a query records. - A caller waiting on another caller's compute can't cancel the wait. See Cancellation.
- FusionCache's factory timeouts and its per-key
DefaultEntryOptionsProviderdon't apply to Sluice calls. See What Sluice sets. - Work started under
ExecutionContext.SuppressFlowcan't see the compute, so the nesting and read checks are off there. See What isn't checked.
Multi-node
- Clocks that disagree between servers open a window about as wide as the difference: FusionCache compares when one server stored an entry with when another invalidated it.
- A missed backplane message leaves that server serving old values until its in-memory tag data expires (an hour by default). Without a distributed cache, only the entries' own expiry ends it.
- A Redis blip while a server first looks a tag up can make it treat the tag as never invalidated until that lookup expires, even if it missed no message.
- A slow Redis delays every invalidation, and every EF save, until the Redis client times out. See When Redis is slow or down.
EF Core
- A database cascade evicts whole types. When a delete makes the database delete rows of other types, or set their foreign key to null, every
Entityread and plain query of those types is evicted, whichever rows it hit. See Database cascades. - Cascades the model doesn't know about (triggers, a database that differs from the model) aren't seen at all. See Database cascades.
- Without a row version, updating or deleting a row evicts its
Childrenlists under every parent, since the parent EF loaded may not be the one the row leaves. Map a row version (xminon PostgreSQL,rowversionon SQL Server) to keep that exact. See Row versions. - A row saved from an object you built yourself, of a type with a row version, misses its real old parent when its foreign key differs from the stored one. See Rows saved without loading them.
- Two entity types splitting one table and sharing a column: saving one doesn't evict the other's readers. See Model shapes.
- Two classes mapped to the same table aren't linked. Dependencies belong to entity types (classes), not tables. A second class over the same table, such as a read model in another context, is a separate type: a write through either doesn't evict reads through the other, both ways. To link them, have each one's reads also record the other's
AllDependencies()withread.From, or invalidate by hand. See What a query records. - Broad eviction, by design: a plain query is evicted by any write to any type it reads, a
Childrenlist by a change to any row in it, and every read of a type by anExecuteUpdateorExecuteDeleteon its table. See What a query records and Bulk updates and deletes. - Commits Sluice doesn't see: a raw
DbTransactioncommitted outside EF, and someTransactionScopesetups. See Transactions. - Interceptors that change entities in
SavingChangesmust come beforeUseSluice. See Interceptor order. - Relational providers only, and no
UseInternalServiceProvider. Computes take the context from a DI scope of their own, so it must be registered as a scoped service (every EF registration does that). See Setup. - Tested on PostgreSQL only. Key folding assumes PostgreSQL's comparison rules, and doesn't fold accent- or culture-sensitive collations. See Key folding.
- Composite primary keys can't be read with
read.Entity, andread.Childrentakes a single-column foreign key. See Keys. - Views, keyless types, functions and SQL-query mappings can't be read inside a compute except in a fused fetch. See What's refused.
What a compute can't read
- Some reads go unchecked inside a compute, such as
Findof an entity a captured outside context already tracks, or Dapper on a context's connection. See What isn't checked. - Some reads only work in a fused fetch: raw SQL, views, keyless types and your own database functions. See The declared route.
- Query filters that read the context's state need the compute to set it, from the query key, with
read.Db<AppDb>(db => db.TenantId = tenantId); Sluice can't check what the configure sets. Filters that read it where the configure can't set it (through a service the context holds, a property with a body, a method, a readonly field, or a virtual, init-only or get-only property) are refused. See Tenants. - Invalidation isn't per tenant. A write in one tenant evicts other tenants' cached reads of the same entity type, unless they read through
read.Childrenon a tenant foreign key. - Entities that load lazily or hold their context can't be returned by a query in a compute. Select the values you need. See What throws inside a compute.
- Writes can't run inside a compute. See Writes.