Table of Contents

Class QueryCache

Namespace
CL.MySQL2.Services
Assembly
CL.MySQL2.dll

Facade over ICacheStore providing query-result caching with two key properties:

  • Time-quantized keys — DateTime parameters derived from UtcNow are rounded to a configurable window before hashing, so .Where(x => x.At >= UtcNow.AddDays(-30)) no longer produces a unique key per call.
  • Table-version invalidation — mutations bump a per-table version counter that participates in the cache key; prior entries simply become un-hittable and are swept on eviction. No need to track-and-evict individual keys.

Keeps a static facade so existing callers (QueryBuilder, Repository) compile unchanged.

public static class QueryCache
Inheritance
QueryCache
Inherited Members

Properties

Count

Total cached entries (best-effort).

public static int Count { get; }

Property Value

int

Methods

Clear()

Clear the entire cache (admin / tests).

public static void Clear()

Configure(bool, int, int, int, bool)

Apply runtime configuration from CacheConfiguration.

public static void Configure(bool enabled, int maxEntries, int timeQuantizeSeconds, int defaultTtlSeconds = 60, bool publishEvents = true)

Parameters

enabled bool
maxEntries int
timeQuantizeSeconds int
defaultTtlSeconds int
publishEvents bool

GetOrSetAsync<T>(string, string, Func<Task<T>>, TimeSpan, string?)

Cache-aside helper. Returns the cached value if fresh; otherwise runs factory, stores the result, and returns it.

Failure CodeLogic.Core.Results.Result<T> values are NEVER cached and never served as cache hits — a transient DB failure during a cold-warmup cannot poison the cache. If a previously-cached value is detected as a failure (legacy entries from older versions), it's evicted on read so the call falls through to a fresh execution.

public static Task<T> GetOrSetAsync<T>(string cacheKey, string tableName, Func<Task<T>> factory, TimeSpan ttl, string? connectionId = null)

Parameters

cacheKey string
tableName string
factory Func<Task<T>>
ttl TimeSpan
connectionId string

Returns

Task<T>

Type Parameters

T

GetStats()

Diagnostic snapshot: total entries + per-table counts + table-version map. Returned types are immutable copies; safe to log or render on an admin page without holding cache locks.

public static QueryCacheStats GetStats()

Returns

QueryCacheStats

Invalidate(string)

Invalidate all cached entries for the given table.

Two-step: bump the per-table version (so future reads compute a different cache key and miss any in-flight refresh writes), then evict the now-orphaned entries from the underlying store so they don't accumulate. Without the second step, every mutation leaves behind the previous version's entries until TTL/LRU clears them, which on a busy app produces unbounded memory growth.

When a table has active SmartCachePool entries, the pool's background refresh is the source of truth for freshness — it re-executes queries every RefreshEvery and writes to the current version's cache key. Flushing the cache on every mutation (e.g. a stats ingest INSERT) would defeat the pool: every request between the flush and the next refresh tick hits the DB cold. For pool-managed tables we skip the eviction and let the pool refresh deliver naturally-stale-within-interval reads instead.

public static void Invalidate(string tableName)

Parameters

tableName string

Invalidate<T>()

Invalidate for the table behind entity type T.

public static void Invalidate<T>() where T : class

Type Parameters

T

SetConnectionOverride(string, bool?)

Registers (or clears, when enabled is null) a per-database override of the global CL.MySQL2.Services.QueryCache.Enabled switch. Called by MySQL2Library at init.

public static void SetConnectionOverride(string connectionId, bool? enabled)

Parameters

connectionId string
enabled bool?

SetDirectAsync(string, object, TimeSpan, string, CancellationToken)

Direct write into the cache, bypassing the cache-aside flow. Used by SmartCachePool to overwrite an entry after a background refresh produces a fresh value. No observability event is emitted — the refresh isn't a cache hit or miss from a caller's perspective.

public static Task SetDirectAsync(string cacheKey, object value, TimeSpan ttl, string tableName, CancellationToken ct = default)

Parameters

cacheKey string
value object
ttl TimeSpan
tableName string
ct CancellationToken

Returns

Task

UseCoordinator(ICacheCoordinator)

Installs a multi-node ICacheCoordinator (e.g. a Redis pub/sub adapter) so mutations fan out to peers and smart-cache pools refresh single-flight. Wires the coordinator's peer-invalidation callback to the local (non-broadcasting) invalidation path. Pair with a shared ICacheStore via UseStore(ICacheStore).

public static void UseCoordinator(ICacheCoordinator coordinator)

Parameters

coordinator ICacheCoordinator

UseStore(ICacheStore)

Replace the underlying store (e.g. with a Redis adapter).

public static void UseStore(ICacheStore store)

Parameters

store ICacheStore