Codebelt
v10.5.5

Cuemon.Runtime.Caching

Process-local cache primitives with namespace-aware keys, explicit invalidation rules, and a lightweight SlimMemoryCache implementation.

.NET 10.0 / .NET 9.0 / .NET Standard 2.0 MIT 88,049 downloads

Overview

Cuemon.Runtime.Caching defines a small cache contract and a concrete in-memory cache for application code that wants explicit control over key scoping, insertion semantics, and expiration rules. Its main type, SlimMemoryCache, stores entries in a concurrent dictionary, lets callers scope entries with an optional namespace, and supports absolute expiration, sliding expiration, or dependency-driven invalidation through CacheInvalidation.

The package stays close to cache mechanics instead of adding higher-level memoization helpers. Consumers can work directly with SlimMemoryCache, abstract against ICacheEnumerable<long>, or use CachingManager.Cache when a process-wide singleton cache is sufficient.

Key APIs

SlimMemoryCache is the package's concrete cache implementation. It derives from Disposable, implements ICacheEnumerable<long>, accepts an optional Action<SlimMemoryCacheOptions> setup delegate, and uses a configurable Func<string, string, long> key provider to map the caller's key and namespace into the underlying concurrent dictionary.

SlimMemoryCache.Add(...) inserts new entries without overwriting an existing key. The overload set covers absolute expiration, sliding expiration, dependency-based invalidation, and the lower-level Add(CacheEntry, CacheInvalidation) form when callers need to construct the entry metadata themselves.

SlimMemoryCache.TryGet(...) and TryGetCacheEntry(...) are the read paths that enforce expiration rules. They refresh the Accessed timestamp for live entries, remove expired entries on lookup, and let callers choose between retrieving the cached value or the full CacheEntry metadata.

SlimMemoryCache.Set(...) is the update-oriented companion to Add(...). It inserts a missing entry or updates an existing one in place, while refreshing the access timestamp when the key is already present.

CacheInvalidation is the package's eviction model. Its constructors make the invalidation mode explicit, and the sliding-expiration overload validates that the interval is greater than zero and does not exceed one year.

SlimMemoryCacheOptions controls the cache's housekeeping behavior. By default it enables automated cleanup, runs the first sweep after 30 seconds, runs succeeding sweeps every two minutes, and builds namespace-aware hash keys with Generate.HashCode64.

CachingManager.Cache exposes a lazily created singleton SlimMemoryCache. It is the simplest entry point when you want one shared in-memory cache for the current process and do not need to manage the cache lifetime yourself.

Basic usage

using System;
using Codebelt.Extensions.Xunit;
using Cuemon.Runtime.Caching;
using Xunit;

namespace Contoso.Inventory.Tests;

public class SlimMemoryCacheUsageTest : Test
{
    public SlimMemoryCacheUsageTest(ITestOutputHelper output) : base(output)
    {
    }

    [Fact]
    public void ShouldReuseTenantScopedSnapshotUntilCacheIsRefreshed()
    {
        var cache = new SlimMemoryCache(o => o.EnableCleanup = false);
        var sut = new CatalogSnapshotStore(cache);

        var tenantA1 = sut.GetSnapshot("tenant-a");
        var tenantA2 = sut.GetSnapshot("tenant-a");
        var tenantB = sut.GetSnapshot("tenant-b");
        var tenantAEntries = cache.Count("tenant-a");
        var tenantBEntries = cache.Count("tenant-b");

        Assert.Same(tenantA1, tenantA2);
        Assert.NotSame(tenantA1, tenantB);
        Assert.Equal(1, tenantAEntries);
        Assert.Equal(1, tenantBEntries);
        TestOutput.WriteLine($"Cached namespaces: tenant-a={tenantAEntries}, tenant-b={tenantBEntries}");
    }

    private sealed class CatalogSnapshotStore
    {
        private readonly SlimMemoryCache _cache;

        public CatalogSnapshotStore(SlimMemoryCache cache)
        {
            _cache = cache;
        }

        public object GetSnapshot(string tenant)
        {
            if (_cache.TryGet("catalog-snapshot", tenant, out var cached)) { return cached; }
            var snapshot = new { Tenant = tenant, Version = Guid.NewGuid() };
            _cache.Set("catalog-snapshot", snapshot, new CacheInvalidation(TimeSpan.FromMinutes(5)), tenant);
            return snapshot;
        }
    }
}

Use this pattern when a service needs a process-local cache but still needs clear tenant or feature boundaries for its keys. It matters because the namespace argument and explicit invalidation policy let the service reuse data without collapsing unrelated cache entries into the same bucket.

Installation

dotnet add package Cuemon.Runtime.Caching

Usage guidance

Choose this package when you want a lightweight in-memory cache with explicit control over key scope, value updates, and expiration behavior inside a single process. If you want higher-level memoization and GetOrAdd helpers, move up to Cuemon.Extensions.Runtime.Caching; if you need a shared or distributed cache across processes, this package does not provide that infrastructure.

Family packages