Mostlylucid.Ephemeral.Atoms.SlidingCache
2.3.2
dotnet add package Mostlylucid.Ephemeral.Atoms.SlidingCache --version 2.3.2
NuGet\Install-Package Mostlylucid.Ephemeral.Atoms.SlidingCache -Version 2.3.2
<PackageReference Include="Mostlylucid.Ephemeral.Atoms.SlidingCache" Version="2.3.2" />
<PackageVersion Include="Mostlylucid.Ephemeral.Atoms.SlidingCache" Version="2.3.2" />
<PackageReference Include="Mostlylucid.Ephemeral.Atoms.SlidingCache" />
paket add Mostlylucid.Ephemeral.Atoms.SlidingCache --version 2.3.2
#r "nuget: Mostlylucid.Ephemeral.Atoms.SlidingCache, 2.3.2"
#:package Mostlylucid.Ephemeral.Atoms.SlidingCache@2.3.2
#addin nuget:?package=Mostlylucid.Ephemeral.Atoms.SlidingCache&version=2.3.2
#tool nuget:?package=Mostlylucid.Ephemeral.Atoms.SlidingCache&version=2.3.2
Mostlylucid.Ephemeral.Atoms.SlidingCache
Caches work results with sliding expiration - accessing a result resets its TTL. Results that haven't been accessed expire and are recomputed on next request.
dotnet add package mostlylucid.ephemeral.atoms.slidingcache
Quick Start
using Mostlylucid.Ephemeral.Atoms.SlidingCache;
await using var cache = new SlidingCacheAtom<string, UserProfile>(
async (userId, ct) => await LoadUserProfileAsync(userId, ct),
slidingExpiration: TimeSpan.FromMinutes(5));
// First call: computes and caches
var profile = await cache.GetOrComputeAsync("user-123");
// Second call within 5 minutes: returns cached, resets TTL
var cached = await cache.GetOrComputeAsync("user-123");
// After 5 minutes of no access: entry expires, recomputes
All Options
new SlidingCacheAtom<TKey, TResult>(
// Required: async factory to compute values
factory: async (key, ct) => await ComputeAsync(key, ct),
// Time without access before entry expires
// Default: 5 minutes
slidingExpiration: TimeSpan.FromMinutes(5),
// Maximum time entry can live regardless of access
// Default: 1 hour
absoluteExpiration: TimeSpan.FromHours(1),
// Maximum cache entries
// Default: 1000
maxSize: 1000,
// Max concurrent factory calls
// Default: Environment.ProcessorCount
maxConcurrency: 8,
// Signal sampling rate (1 = all, 10 = 1 in 10)
// Default: 1
sampleRate: 1,
// Shared signal sink
// Default: null (creates internal)
signals: sharedSink
)
API Reference
// Get or compute value (resets sliding expiration on hit)
Task<TResult> GetOrComputeAsync(TKey key, CancellationToken ct = default);
// Try get without triggering computation (still resets TTL on hit)
bool TryGet(TKey key, out TResult? value);
// Invalidate specific entry
void Invalidate(TKey key);
// Clear all entries
void Clear();
// Get statistics
CacheStats GetStats(); // (TotalEntries, ValidEntries, ExpiredEntries, HotEntries, MaxSize)
// Get signals
IReadOnlyList<SignalEvent> GetSignals();
IReadOnlyList<SignalEvent> GetSignals(string pattern);
// Dispose
ValueTask DisposeAsync();
How It Works
Sliding vs Absolute Expiration
Entry created at T=0, slidingExpiration=5min, absoluteExpiration=1hr
T=0: [Created] ─────────────────────────────────────────> Absolute deadline: T=60min
LastAccess=T=0
T=3min: [Access] ─> LastAccess=T=3min ─> Sliding deadline: T=8min
T=7min: [Access] ─> LastAccess=T=7min ─> Sliding deadline: T=12min
T=15min: [No access since T=7min] ─> Entry EXPIRED (sliding)
T=59min: [Access after recompute] ─> New entry, LastAccess=T=59min
T=61min: Entry EXPIRED (absolute deadline from T=59min creation)
Eviction Strategy
When cache exceeds maxSize:
- First pass: Remove all expired entries
- Second pass: Remove coldest entries (lowest access count, then oldest access time)
Signals Emitted
| Signal | Description |
|---|---|
cache.hit:{key} |
Cache hit, returned cached value |
cache.miss:{key} |
Cache miss, computing value |
cache.peek:{key} |
TryGet hit without computation |
cache.hit.dedup:{key} |
Hit during deduplication check |
cache.compute.start:{key} |
Starting factory computation |
cache.compute.done:{key} |
Factory computation complete |
cache.invalidate:{key} |
Manual invalidation |
cache.clear:{count} |
All entries cleared |
cache.evict.expired:{key} |
Evicted due to expiration |
cache.evict.cold:{key} |
Evicted due to size limit (cold entry) |
cache.error:{key}:{type} |
Factory threw exception |
Example: API Response Caching
await using var cache = new SlidingCacheAtom<string, ApiResponse>(
async (endpoint, ct) =>
{
var response = await httpClient.GetAsync(endpoint, ct);
return await response.Content.ReadFromJsonAsync<ApiResponse>(ct);
},
slidingExpiration: TimeSpan.FromMinutes(2),
absoluteExpiration: TimeSpan.FromMinutes(30),
maxConcurrency: 4);
// Multiple concurrent requests for same endpoint are deduplicated
var tasks = Enumerable.Range(0, 10)
.Select(_ => cache.GetOrComputeAsync("/api/users"));
var results = await Task.WhenAll(tasks);
// Only 1 HTTP call made, all 10 tasks get same result
Example: Database Query Caching
await using var cache = new SlidingCacheAtom<int, Order>(
async (orderId, ct) => await db.Orders.FindAsync(orderId, ct),
slidingExpiration: TimeSpan.FromMinutes(10),
maxSize: 5000,
sampleRate: 10); // Sample 1 in 10 for high-volume
// Hot orders stay cached, cold orders expire
var order = await cache.GetOrComputeAsync(orderId);
// Monitor cache health
var stats = cache.GetStats();
Console.WriteLine($"Hit rate estimate: {stats.HotEntries}/{stats.TotalEntries} hot");
// Check for errors
var errors = cache.GetSignals("cache.error:*");
if (errors.Any())
logger.LogWarning("Cache errors: {Count}", errors.Count);
Example: With Shared Signal Sink
var sink = new SignalSink();
await using var userCache = new SlidingCacheAtom<string, User>(
LoadUserAsync,
signals: sink);
await using var orderCache = new SlidingCacheAtom<int, Order>(
LoadOrderAsync,
signals: sink);
// Monitor all cache activity
var allMisses = sink.Sense(s => s.Signal.StartsWith("cache.miss"));
var allErrors = sink.Sense(s => s.Signal.StartsWith("cache.error"));
Related Packages
| Package | Description |
|---|---|
| mostlylucid.ephemeral | Core library |
| mostlylucid.ephemeral.atoms.retry | Retry with backoff |
| mostlylucid.ephemeral.complete | All in one DLL |
Cache Strategy Comparison
Use the right cache for the job:
| Cache | Expiration Model | Specialization | Notes |
|---|---|---|---|
SlidingCacheAtom |
Sliding on every hit + absolute max lifetime | Dedupes concurrent computes; emits signals | Best for async work results where every access should refresh TTL. |
EphemeralLruCache (default in sqlite helper) |
Sliding on every hit; hot keys extend TTL further | Hot detection (cache.hot) and LRU-style cleanup |
Lives in core; used by SqliteSingleWriter for self-focusing caches. |
MemoryCache in SqliteSingleWriter |
Sliding only (via MemoryCacheEntryOptions) |
None | (Replaced by EphemeralLruCache as the default.) |
Tip: Default SQLite helper uses
EphemeralLruCachefor hot-key bias; reach forSlidingCacheAtomwhen you need async factories with sliding expiration and dedupe.
License
Unlicense (public domain)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- mostlylucid.ephemeral (>= 2.3.2)
-
net8.0
- mostlylucid.ephemeral (>= 2.3.2)
-
net9.0
- mostlylucid.ephemeral (>= 2.3.2)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Mostlylucid.Ephemeral.Atoms.SlidingCache:
| Package | Downloads |
|---|---|
|
mostlylucid.botdetection
Bot detection middleware for ASP.NET Core applications with behavioral analysis, header inspection, IP-based detection, and optional LLM-based classification. |
|
|
mostlylucid.ephemeral.complete
Meta-package that references all Mostlylucid.Ephemeral packages - bounded async execution with signals, atoms, and patterns. Install this single package to get everything. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.3.2 | 1,782 | 1/9/2026 |
| 2.3.1 | 107 | 1/9/2026 |
| 2.3.1-alpha0 | 97 | 1/9/2026 |
| 2.3.0 | 814 | 1/8/2026 |
| 2.3.0-alpha1 | 99 | 1/8/2026 |
| 2.1.0 | 106 | 1/8/2026 |
| 2.1.0-preview | 97 | 1/8/2026 |
| 2.0.1 | 105 | 1/8/2026 |
| 2.0.0 | 147 | 1/8/2026 |
| 2.0.0-alpha1 | 94 | 1/8/2026 |
| 2.0.0-alpha0 | 98 | 1/8/2026 |
| 1.7.1 | 425 | 12/11/2025 |
| 1.6.8 | 445 | 12/9/2025 |
| 1.6.7 | 435 | 12/9/2025 |
| 1.6.6 | 435 | 12/9/2025 |
| 1.6.5 | 441 | 12/9/2025 |
| 1.6.0 | 420 | 12/8/2025 |
| 1.5.0 | 421 | 12/8/2025 |
| 1.3.0 | 303 | 12/7/2025 |
| 1.2.2 | 300 | 12/7/2025 |