Skip to content

Replace custom ICacheStore with FusionCache#111

Merged
antosubash merged 3 commits intomainfrom
feature/condescending-bassi
Apr 15, 2026
Merged

Replace custom ICacheStore with FusionCache#111
antosubash merged 3 commits intomainfrom
feature/condescending-bassi

Conversation

@antosubash
Copy link
Copy Markdown
Owner

Summary

Replace the custom framework/SimpleModule.Core/Caching/ abstraction (8 files, 461 LOC) with ZiggyCreatures.FusionCache 2.6.0. The custom cache provided in-process IMemoryCache with per-key stampede locking, prefix-based removal, and key tracking — all of which FusionCache provides natively with more battle-testing.

  • −780 LOC net (907 deletions, 127 insertions across 25 files)
  • 11 call sites migrated across Settings, FeatureFlags, Localization, Marketplace, Permissions, Tenants
  • Zero module csproj edits — FusionCache flows transitively from SimpleModule.Core
  • RemoveByPrefixAsync was only used in the deleted test — no prefix→tag conversion needed in production

Migration map

Old New
ICacheStore IFusionCache
GetOrCreateAsync(key, factory, options) GetOrSetAsync(key, factory, opts => opts.Duration = ...)
TryGetAsync<T> + SetAsync pairs (unconditional) GetOrSetAsync (single call)
TryGetAsync<T> + SetAsync pairs (conditional) kept as TryGetAsync + SetAsync (LocaleResolutionMiddleware)
CacheEntryOptions.Expires(duration) opts => opts.Duration = duration
CacheKey.Compose(...) (1 site) inline string interpolation
AddSimpleModuleCaching() AddFusionCache().WithDefaultEntryOptions(...)

Stampede prevention is built into FusionCache's StandardMemoryLocker — no per-key SemaphoreSlim management required.

Test plan

  • dotnet build — 0 warnings, 0 errors
  • dotnet test — 1207 passed, 0 failed, 1 skipped (pre-existing)
  • npm run check — biome + typecheck + i18n + validate-pages all green
  • npm run build — all 19 modules + ClientApp build clean
  • npm run test:smoke — 68/68 E2E smoke tests pass
  • Zero stale references to ICacheStore, MemoryCacheStore, CacheEntryOptions, CacheResult, CacheKey.Compose, AddSimpleModuleCaching anywhere in the repo

Part of a series

This is PR 1 of 3 replacing custom framework code with trusted OSS libraries:

  1. [this PR] Custom cache → FusionCache
  2. Custom ValidationBuilder → FluentValidation
  3. Custom IEventBus → Wolverine

Each PR is independent and can merge in any order.

Delete the entire framework/SimpleModule.Core/Caching/ tree (8 files, 461 LOC)
and its test (MemoryCacheStoreTests, 300 LOC). Stampede protection, per-key
locking, key tracking, and prefix removal are all provided natively by
ZiggyCreatures.FusionCache 2.6.0 — the custom abstraction added no value
over the upstream library.

Migrate the 11 call sites across Settings, FeatureFlags, Localization,
Marketplace, Permissions, and Tenants to IFusionCache. TryGet + Set pairs
become GetOrSetAsync where the factory unconditionally caches its result;
the LocaleResolutionMiddleware keeps its TryGet/Set pattern because it
conditionally caches (explicit user setting yes, fallback no).

No module csproj needs editing — FusionCache flows transitively from
SimpleModule.Core. RemoveByPrefixAsync was only used by the deleted test,
so no prefix→tag conversion is required in production code.

Net: -774 LOC. All 1207 tests green.
The Action<FusionCacheEntryOptions> setup-action overload passed to
GetOrSetAsync/SetAsync is invoked on every call (even on cache hits), so
'options => options.Duration = X' allocates a new closure each call. On
hot paths this shows up:

- PermissionClaimsTransformation.TransformAsync runs per authenticated
  request through the auth pipeline
- HostNameTenantResolver.ResolveAsync runs per request
- LocaleResolutionMiddleware runs per request (SetAsync only fires on
  miss, but still worth cleaning up for consistency)

Replace the TimeSpan-field-plus-lambda pattern with a pre-built
FusionCacheEntryOptions instance that is passed directly to the cache
API. For services whose durations come from IOptions<T>, initialize
a readonly instance field from the primary-constructor parameter so
the instance is built once per service lifetime rather than per call.

Also drop redundant 'var duration = ...' locals and the
'opts => opts.Duration = ...' pattern across every call site; the
pre-built options instance eliminates both concerns.

FusionCache internally calls Duplicate() on the passed options before
applying per-call modifications, so sharing an instance across calls
is safe.

All 1207 tests pass.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 15, 2026

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7079d51
Status: ✅  Deploy successful!
Preview URL: https://bd531668.simplemodule-website.pages.dev
Branch Preview URL: https://feature-condescending-bassi.simplemodule-website.pages.dev

View logs

…-bassi

# Conflicts:
#	modules/FeatureFlags/src/SimpleModule.FeatureFlags/FeatureFlagService.cs
@antosubash antosubash merged commit 6d2513d into main Apr 15, 2026
5 checks passed
@antosubash antosubash deleted the feature/condescending-bassi branch April 15, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant