Table of Contents

Getting started

Install

dotnet add package Tharga.Cache

The core package caches in memory and has no external dependencies. Add a backend package only when you want a type to persist outside the process — see Persistence backends.

Register

builder.Services.AddCache();

AddCache is idempotent — calling it more than once (for example when several libraries each register cache types) merges the registrations instead of throwing.

The get-or-load pattern

Inject one of the four cache interfaces and call GetAsync with a key and a fetch delegate. The first call runs the delegate and stores the result; subsequent calls within the fresh span return the cached value without invoking the delegate.

public class WeatherService(ITimeToLiveCache cache)
{
    public Task<WeatherForecast[]> GetForecastAsync() =>
        cache.GetAsync<WeatherForecast[]>(
            "weather-forecast",
            () => LoadFromApiAsync(),
            TimeSpan.FromMinutes(5));
}

Pick the interface that matches the lifetime you need — IEternalCache, ITimeToLiveCache, ITimeToIdleCache, or IScopeCache. See Cache types.

Common operations

Every cache type shares these operations from ICache:

// Get or load
var data = await cache.GetAsync<MyData>("key", () => FetchDataAsync());

// Peek without triggering a load (returns default if not cached)
var cached = await cache.PeekAsync<MyData>("key");

// Manually set a value
await cache.SetAsync<MyData>("key", myData);

// Remove a specific item
await cache.DropAsync<MyData>("key");

// Mark as stale (triggers reload on next access)
await cache.InvalidateAsync<MyData>("key");

Time-based caches (ITimeToLiveCache, ITimeToIdleCache) also take an explicit fresh span:

var data = await timeCache.GetAsync<MyData>("key", () => FetchAsync(), TimeSpan.FromMinutes(5));
await timeCache.SetAsync<MyData>("key", myData, TimeSpan.FromHours(1));

The no-span SetAsync<T>(key, data) overload requires DefaultFreshSpan to be configured on the type registration. The explicit-span overload works unconditionally.

Configuration

Per-type options

Use RegisterType to configure behavior for a specific cached type:

builder.Services.AddCache(o =>
{
    o.RegisterType<Product, IMemory>(t =>
    {
        t.DefaultFreshSpan = TimeSpan.FromMinutes(10);
        t.StaleWhileRevalidate = true;
        t.MaxCount = 1000;
        t.MaxSize = Size.MB * 100;
        t.EvictionPolicy = EvictionPolicy.LeastRecentlyUsed;
    });
});
Option Default Description
DefaultFreshSpan null Default TTL when not specified per call
StaleWhileRevalidate false Return stale data immediately while refreshing in the background
ReturnDefaultOnFirstLoad false Return default(T) on first cache miss instead of blocking; factory runs in the background
MaxCount null Maximum number of cached items for this type
MaxSize null Maximum total size in bytes for this type
EvictionPolicy FirstInFirstOut Strategy when MaxCount or MaxSize is exceeded

Global options

builder.Services.AddCache(o =>
{
    o.MaxConcurrentFetchCount = 20;               // Max parallel background fetches (default: 10)
    o.WatchDogInterval = TimeSpan.FromMinutes(2); // Stale-cleanup interval (default: 60s)

    o.Default = new CacheTypeOptions             // Defaults applied to all types
    {
        DefaultFreshSpan = TimeSpan.FromSeconds(30)
    };
});

Eviction policies

When MaxCount or MaxSize is exceeded, items are evicted according to the configured policy:

Policy Description
FirstInFirstOut Removes the oldest items first (default)
LeastRecentlyUsed Removes items that haven't been accessed recently
RandomReplacement Removes items at random (lowest overhead)

Size constants

Use the Size helper for readable byte values — Size.KB, Size.MB, Size.GB, Size.TB:

t.MaxSize = Size.MB * 500;   // 500 MB
t.MaxSize = Size.GB * 2;     // 2 GB

Key building

Cache keys can be simple strings or composed from multiple parts with KeyBuilder:

// Simple string key (implicit conversion)
Key key = "my-cache-key";

// Composite key from multiple parts
var key = KeyBuilder
    .Set("userId", userId)
    .Set("department", department);

var data = await cache.GetAsync<UserProfile>(key, () => LoadProfileAsync(userId, department));

Next steps