DynamicWhere.ex
DynamicWhere.exv3.4.0·docs

Cache Architecture

The cache subsystem is split into six single-responsibility components. Only one of them — CacheExpose — is part of the public API; the other five are implementation details you can read about for context.

The six components

ComponentResponsibility
CacheReflectionCore reflection operations with caching — turns a type or path lookup into a cache hit (or a fresh reflection call on miss).
CacheDatabaseThread-safe ConcurrentDictionary stores & per-entry access tracking (timestamps for LRU, access counts for LFU).
CacheEvictionThe FIFO, LRU, and LFU eviction algorithms. Runs when a store crosses MaxCacheSize or when forced.
CacheReportingRenders statistics, memory usage, and performance reports — every report string and the monitoring dictionary come from here.
CacheCalculatorMemory estimation — walks live cache entries and sizes them from fixed constants to produce a CacheMemoryUsage snapshot. Nothing is measured from the GC.
CacheExposePublic API — the only class consumers interact with. Every method you can call on the cache lives here.
Note
CacheExpose is the only class you call. The other five are internal classes inside that same public namespace and may change without notice. The rest of the public surface — 16 types in all — is data you pass in or read back: CacheOptions, the two enums, and the result types.

Namespaces

The public surface lives in six namespaces:

using DynamicWhere.ex.Optimization.Cache.Source;   // CacheExpose
using DynamicWhere.ex.Optimization.Cache.Config;   // CacheOptions
using DynamicWhere.ex.Optimization.Cache.Enums;    // CacheEvictionStrategy, CacheMemoryType
using DynamicWhere.ex.Optimization.Cache.DTOs;     // CacheStatistics, CacheConfiguration, CacheMemoryUsage, ...
using DynamicWhere.ex.Optimization.Cache.Input;    // HealthAlertsInput, CacheFullCheckInput, ...
using DynamicWhere.ex.Optimization.Cache.Output;   // CacheCounts, TrackingCounts, CacheDatabases

Flow of a cached lookup

When a query asks for the resolved property path "Customer.Address.City", the components cooperate as follows:

  1. CacheReflection receives the lookup request.
  2. It asks CacheDatabase for the cached path. A hit skips the next two steps.
  3. On a miss, CacheEviction runs first: if the store already holds more than MaxCacheSize entries, the configured algorithm trims it.
  4. CacheReflection then performs the real reflection, validates the path, normalises the casing, and writes the result into CacheDatabase. The eviction pass runs before that write, so a store settles at MaxCacheSize + 1 entries. A path that fails validation throws here, and nothing is written.
  5. Only a path that has validated records an access under the active strategy — a timestamp for LRU, a counter for LFU, nothing for FIFO. A path that fails records nothing (3.1.0), so an invented name leaves no record behind. Under LRU the timestamp is refreshed once it is a second old rather than on every read (3.3.0).
  6. A lookup takes no lock and a hit allocates nothing (3.3.0). The configuration in force is read with one volatile read, where every lookup used to lock and copy it; GetCacheConfigOptions() still returns a copy, because an instance a caller edited would be the one in force.
  7. CacheReporting and CacheCalculator are read-only consumers of CacheDatabase — they never mutate cache state.