DcsvIo.D2.Caching.Tiered
0.1.2
dotnet add package DcsvIo.D2.Caching.Tiered --version 0.1.2
NuGet\Install-Package DcsvIo.D2.Caching.Tiered -Version 0.1.2
<PackageReference Include="DcsvIo.D2.Caching.Tiered" Version="0.1.2" />
<PackageVersion Include="DcsvIo.D2.Caching.Tiered" Version="0.1.2" />
<PackageReference Include="DcsvIo.D2.Caching.Tiered" />
paket add DcsvIo.D2.Caching.Tiered --version 0.1.2
#r "nuget: DcsvIo.D2.Caching.Tiered, 0.1.2"
#:package DcsvIo.D2.Caching.Tiered@0.1.2
#addin nuget:?package=DcsvIo.D2.Caching.Tiered&version=0.1.2
#tool nuget:?package=DcsvIo.D2.Caching.Tiered&version=0.1.2
DcsvIo.D2.Caching.Tiered
Two-tier cache that composes one ILocalCache (L1) and one
IDistributedCache (L2) into the
ITieredCache marker interface.
Install
dotnet add package DcsvIo.D2.Caching.Tiered
Public surface
DefaultTieredCache : ITieredCache, IAsyncDisposable — the implementation.
services.AddD2TieredCache() — DI registration. Singleton. Requires
ILocalCache + IDistributedCache to be registered first.
Behavior
Reads (GetAsync / GetManyAsync / ExistsAsync):
- Try L1 first
- On L1 miss, fall through to L2
- On L2 hit, populate L1 with the value (next read on this instance is sub-microsecond)
- Return the result
Writes (SetAsync / SetManyAsync / RemoveAsync / RemoveManyAsync):
- L2 first — if L2 fails, return that failure and do not touch L1
- L1 second — only fires after L2 succeeded
- If L1 fails after L2 succeeded, the L2-success result is what's returned — the L1 failure is
logged at Warning (
L1WriteFailedAfterL2Success) and swallowed. Rationale (§18 graceful degradation): L1 is the optional layer; an L1 sneeze on this instance must not fail a write the cluster has successfully accepted. Next read on this instance re-fetches from L2. - L2-first ordering means partial-write states are impossible; the result is binary (L2's outcome, with any L1 noise logged separately).
Atomic primitives (SetNxAsync / IncrementAsync / AcquireLockAsync / ReleaseLockAsync):
- Route through L2 (the cluster source of truth)
- After L2 succeeds, invalidate L1 so the next read fetches the canonical value
- For
SetNxAsync: if L2 took the write, populate L1 with the same value; if L2 already had the key, drop our (potentially stale) L1 - For
IncrementAsync: always invalidate L1 (counters held in L1 would diverge from L2 immediately as other instances increment) - For
AcquireLockAsync/ReleaseLockAsync: pure delegation; L1 isn't involved in lock state at all
- For
Broadcast variants (SetAndBroadcastAsync etc.):
- Do the underlying write, then publish via the registered
ICacheInvalidationBackplane - All subscribers (across every connected instance, including this one) receive the key and drop their L1 entry — universal "everyone acts" rule
- The publishing instance also receives its own message and drops L1; next read re-fetches from L2 (one extra round-trip — bounded cost; acceptable for read-heavy use cases)
Backplane subscription
If an ICacheInvalidationBackplane is registered, the tiered cache subscribes to it in its
constructor. Every received invalidation key triggers a RemoveAsync on the L1 cache — that's the
entire L1-coherency mechanism. If the L1 RemoveAsync fails, the failure is logged at Warning
(L1InvalidationFailed) and processing continues; L1 stays stale on this instance until TTL or the
next write/broadcast for that key. The subscription disposes when the tiered cache disposes.
If no backplane is registered, *AndBroadcast* methods throw InvalidOperationException. The plain
ops (SetAsync / RemoveAsync etc.) work fine; L1 caches drift from each other until their TTLs
expire.
Design rationale: no ICacheSet
ITieredCache deliberately does NOT implement ICacheSet (SADD/SCARD). Set-cardinality is
inherently cluster-only — there's no honest way to compose it across L1+L2 (an L1 set would only see
this instance's adds; cluster cardinality lives in L2). Callers needing set primitives inject
IDistributedCache directly.
Test coverage
Integration/Caching/Tiered/ (xunit collection "Redis", shared Testcontainers Redis fixture):
DefaultTieredCacheRedisTests— end-to-end against real L1 (IMemoryCache) + real L2 (Redis):Get_L1Miss_PopulatesFromL2Set_WritesToBoth/Remove_RemovesFromBothSetAndBroadcast_OtherInstanceL1Drops— two tiered instances on the same backplane; A writes, B's L1 receives the invalidation and dropsSetAndBroadcast_OwnInstanceL1AlsoDrops— universal "everyone acts" rule verified end-to-endIncrementAsync_RoutesThroughL2_InvalidatesL1RemoveAndBroadcast_OtherInstancesDropL1
Dependencies
DcsvIo.D2.Caching.Abstractions—ITieredCache,ILocalCache,IDistributedCache,ICacheInvalidationBackplane- No backing-store deps directly — the impl is pure composition
References
DcsvIo.D2.Caching.Abstractions— interface + result-mapping referenceDcsvIo.D2.Caching.Local.Default— typical L1 implDcsvIo.D2.Caching.Distributed.Redis— typical L2 impl
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- DcsvIo.D2.Caching.Abstractions (>= 0.1.1)
- DcsvIo.D2.Result (>= 0.1.1)
- JetBrains.Annotations (>= 2025.2.4)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.