Design decisions and rationale that aren't obvious from reading the source code. For project guidance, see CLAUDE.md.
QueryClient → QueryCache → Query<TData> → QueryObserver<TData, TQueryData> → QueryViewModel
↓ ↓ ↓ ↓ ↓
Orchestrator Storage State Machine Subscription Layer MVVM Wrapper
TanStack Query uses a single mega-object for all options. C# splits this into four types because each serves a different layer with different lifetime and composition needs:
| Type | Why it exists |
|---|---|
QueryConfiguration<TData> |
Cache-level config (GcTime, Retry, InitialData, NetworkMode) passed to QueryCache.Build. Separating this from observer options lets multiple observers share cache config without re-specifying it. |
QueryOptions<TData> |
Reusable query definition (key + fn + config). Analogous to TanStack v5's queryOptions() helper. Covers the 80% case where no Select transform or observer-level options are needed. |
QueryObserverOptions<TData, TQueryData> |
Observer-level config (Select, RefetchInterval, Enabled, PlaceholderData). A record so with expressions can toggle individual properties (e.g., switching polling on/off). |
FetchQueryOptions<TData> |
Imperative fetch config for FetchQueryAsync/PrefetchQueryAsync/EnsureQueryDataAsync. Exists because these methods don't create persistent observer subscriptions and need different defaults (e.g., Retry = 0). |
JavaScript's getQueryData works regardless of type because there's no runtime type checking. C# queries are Query<TData> — hydration doesn't know TData at restore time.
-
Dehydration:
Querybase hasabstract Dehydrate()producingDehydratedQuerywithobject?data. No type information needed. -
Hydrating new queries: Created as
Query<object>withIsHydratedPlaceholder = true. Discoverable viaFindAll()/GetAll()butGetQueryData<T>()returnsdefault(type mismatch). -
Placeholder upgrade: When
QueryCache.Build<TData>()is called (observer subscription,SetQueryData), it detects the placeholder, removes it, and creates a properly-typedQuery<TData>from the dehydrated state.
-
GetQueryData<T>returnsdefaultfor unupgraded placeholders — TanStack'sgetQueryDataalways returns hydrated data because JavaScript has no runtime type checking. Consumers needing pre-subscription access can useQueryCache.TryGetHydratedData<T>(hash)(internal API). -
Upgrade atomicity — The remove-then-add in
Buildis not atomic. BetweenRemove(placeholder)andAdd(typedQuery), concurrentFindAll/GetByHashcalls won't see the query. Acceptable becauseBuild<TData>is typically called on the UI thread (Blazor/MAUI). -
No promise dehydration — C# Tasks can't transfer across processes. Pending queries hydrate as state-only; observers trigger a fresh fetch.
-
No data transformers — Skip
serializeData/deserializeData. Consumers handle JSON serialization externally. -
No error redaction — Skip
shouldRedactErrors.