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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Mostlylucid.Ephemeral.Atoms.SlidingCache" Version="2.3.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mostlylucid.Ephemeral.Atoms.SlidingCache" Version="2.3.2" />
                    
Directory.Packages.props
<PackageReference Include="Mostlylucid.Ephemeral.Atoms.SlidingCache" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Mostlylucid.Ephemeral.Atoms.SlidingCache --version 2.3.2
                    
#r "nuget: Mostlylucid.Ephemeral.Atoms.SlidingCache, 2.3.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Mostlylucid.Ephemeral.Atoms.SlidingCache@2.3.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Mostlylucid.Ephemeral.Atoms.SlidingCache&version=2.3.2
                    
Install as a Cake Addin
#tool nuget:?package=Mostlylucid.Ephemeral.Atoms.SlidingCache&version=2.3.2
                    
Install as a Cake Tool

Mostlylucid.Ephemeral.Atoms.SlidingCache

NuGet

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:

  1. First pass: Remove all expired entries
  2. 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"));

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 EphemeralLruCache for hot-key bias; reach for SlidingCacheAtom when you need async factories with sliding expiration and dedupe.

License

Unlicense (public domain)

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed