Multi-instance list-cache: IListCache (Redis-backed by default)
Confirmed 2026-08-26 that at least one client deployment runs multiple ADO
Environment VMs behind the same client's Api instance — meaning
IMemoryCache, used per the Reference CRUD pattern's caching step, has a real
staleness bug: a write on instance A invalidates only A's cache; instances
B/C keep serving the pre-write list for up to the full TTL.
:::note Scope was wider than expected
Only City/Country/Choice inherit MasListRepositoryBase; 32 more
Mas* repositories independently hand-rolled the identical
IMemoryCache.GetOrCreateAsync/Cache.Remove pattern inline — 35 files
total, not ~20. Full rollout completed the same day as the pilot.
:::
Building blocks
IListCache(Application/Common/Interfaces/) — the abstraction everyMas*repository injects instead ofIMemoryCache:GetOrCreateAsync<T>(key, factory, ttl, ct)/RemoveAsync(key, ct), generic overIReadOnlyList<T>.RedisListCache(Infrastructure/Caching/) — the distributed implementation, same lazy-connect-on-first-use singleton shape asRedisPendingSsoLoginStore. Reuses the Redis connection settings already onMAS_SYSCONFIGrather than a second config mechanism; values are JSON-serialized into Redis strings keyed{prefix}ListCache:{key}with Redis's own TTL replacingAbsoluteExpirationRelativeToNow. No distributed lock around a cache-miss factory run — a brief thundering-herd re-run of the same idempotent DB query on concurrent misses is the same behaviorIMemoryCache's ownGetOrCreateAsyncalready had per-process.MemoryListCache(Infrastructure/Caching/) — thinIMemoryCachewrapper, the single-instance fallback for local dev with no Redis running.Caching:ListCacheconfig toggle, same convention asSso:PendingLoginStore/Auth:RefreshTokenStore:"Memory"forces the fallback,"Redis"forces Redis, anything else (including unset) defaults to Redis outsideDevelopment, Memory inside it.MasListRepositoryBase<TEntity, TDto>takesIListCacheinstead ofIMemoryCache; derived repositories' write paths callawait Cache.RemoveAsync(key, ct)(was the synchronousCache.Remove(key)).
A handful of methods cache a single value rather than a list
(AppSettingRepository.GetStringValueAsync, LabelTranslationRepository.ResolveAsync,
a few others) — IListCache has no scalar overload, so each wraps its value
in a 1-element list and unwraps it after the call.
:::caution The scaffold tool hasn't been updated
backend/tools/Logiswift.Eos.Scaffold/Generators/RepositoryGenerator.cs
still generates raw IMemoryCache usage — not yet updated to emit
IListCache. A newly scaffolded entity needs this swap done by hand.
:::
:::danger Not verified No live test against an actual multi-instance deployment or a real Redis connection exists yet — same standing "shipped, unexecuted against real infrastructure" caveat as several other items in this codebase. :::
Reference files
IListCache.cs, RedisListCache.cs, MemoryListCache.cs,
MasListRepositoryBase.cs, CityRepository.cs/CountryRepository.cs/
ChoiceRepository.cs (the pilot), RedisPendingSsoLoginStore.cs (the
connection-pattern precedent), DependencyInjection.cs
(ShouldUseRedisListCache).