Cache feature family · v1.3.2

Swap cache providers through config, not a code change

ICacheService is provider-agnostic with a safe-off NoOp default. Application code never changes when the active provider does — only the configuration value does.

$ dotnet add package PowerCSharp.Feature.Cache
ICacheService

One contract, whether the value is a hit, a miss, or null

Sync and async members, stampede protection via GetOrCreate/GetOrCreateAsync, and metadata access via GetMetadata. CacheResult<T> is a readonly struct with Hit, Value, and Metadata — deliberately disambiguating 'not found' from 'found but null'.

ProductService.cs
var cache = app.Services.GetRequiredService<ICacheService>();

await cache.SetAsync("key", myObject, TimeSpan.FromMinutes(5));
var result = await cache.GetAsync<MyObject>("key");

if (result.Hit)
    Console.WriteLine(result.Value);
Two-layer gating

Off by default, safe when it's off

Layer 1 (build-time): don't reference a provider package and its third-party dependency is absent from your dependency tree entirely. Layer 2 (runtime flag): reference it but leave the flag off, and a NoOp implementation registers via TryAddSingleton so dependents still resolve.

Provider selection

PowerFeatures:Cache:Provider

"BitFaster" | "Disk" | "None"

BitFaster

in-memory LRU, requires Feature.Cache.BitFaster

Disk

cross-process LRU, requires Feature.Cache.Disk

NoOp

safe default — always resolves, does nothing

Real usage

Stampede protection without extra code

Concurrent callers requesting the same missing key trigger exactly one factory call — the rest wait for the result instead of duplicating work against your database or upstream API.

ProductService.cs
public class ProductService(ICacheService cache)
{
    public async Task<Product> GetProductAsync(string sku)
    {
        var result = await cache.GetAsync<Product>(sku);
        if (result.Hit)
            return result.Value!;

        var product = await FetchFromCatalogAsync(sku);
        await cache.SetAsync(sku, product, TimeSpan.FromMinutes(10));
        return product;
    }
}

BitFaster vs. Disk

Both implement the identical ICacheService contract — pick based on where the data should live, not how your code calls it.

PowerCSharp.Feature.Cache.BitFaster

In-memory LRU

  • Backed by BitFaster.Caching's ConcurrentLru<K,V> (W-TinyLFU-variant eviction)
  • Lock-free concurrent reads; atomic GetOrAdd gives true stampede protection
  • The only package in the family with a third-party NuGet dependency — fully isolated to it
  • Best for high read-throughput, per-process caching where the dataset fits in RAM

PowerCSharp.Feature.Cache.Disk

Cross-process LRU

  • Zero third-party NuGet dependencies — pure System.IO / System.Text.Json
  • Atomic writes via write-to-temp-then-rename; per-key .lock files for cross-process coordination
  • Sidecar .meta files store metadata separately, readable without deserializing the value
  • Best for datasets larger than RAM, or multiple processes sharing a cache on one host

Install the provider you need

$ dotnet add package PowerCSharp.Feature.Cache.BitFaster
$ dotnet add package PowerCSharp.Feature.Cache.Disk