Overview
Cuemon.Extensions.Runtime.Caching adds read-through caching and delegate memoization to any ICacheEnumerable<TKey> from Cuemon.Runtime.Caching. Instead of repeating TryGet, CacheEntry, and CacheInvalidation plumbing at each call site, consumers can use GetOrAdd for named entries or Memoize to turn an expensive delegate into a cached delegate.
Both APIs accept sliding expiration, absolute expiration, dependency-based invalidation, or a prebuilt CacheInvalidation. The memoization path stores entries in the dedicated MemoizationScope, derives cache keys from the delegate method plus supplied arguments, and uses a shared lock so concurrent callers converge on one cached value.
Key APIs
GetOrAdd<TKey, TResult>(this ICacheEnumerable<TKey> cache, string key, string ns, CacheInvalidation invalidation, Func<TResult> valueFactory) is the core read-through helper. It validates the cache, key, invalidation details, and factory delegate, then either returns the existing value or adds a new CacheEntry with the requested invalidation policy.
GetOrAdd<TKey, TResult>(this ICacheEnumerable<TKey> cache, string key, TimeSpan slidingExpiration, Func<TResult> valueFactory) is the common convenience entry point when callers only need time-based expiry. The sibling overloads for DateTime, IDependency, and IEnumerable<IDependency> all normalize into the same read-through flow without forcing the caller to construct CacheInvalidation manually.
Memoize<TKey, TResult>(this ICacheEnumerable<TKey> cache, CacheInvalidation invalidation, Func<TResult> valueFactory) wraps a parameterless delegate and returns a cached delegate with the same shape. The returned function computes a memoization key from the target method, adds the cached value under MemoizationScope, and reuses the cached result until the invalidation policy expires it.
Memoize<TKey, T, TResult>(this ICacheEnumerable<TKey> cache, CacheInvalidation invalidation, Func<T, TResult> valueFactory) and the overload families up to five parameters extend the same model to argument-sensitive delegates. Each invocation folds the supplied arguments into the memoization key through ComputeMemoizationCacheKey, so repeated calls with the same inputs reuse one cached result while different inputs produce distinct entries.
MemoizationScope identifies the cache namespace reserved for memoized delegates. The tests use it with Count to verify that repeated delegate calls collapse into one entry and that expiration removes the memoized entries when the invalidation window passes.
Basic usage
using System;
using Cuemon.Extensions.Runtime.Caching;
using Cuemon.Runtime.Caching;
using Codebelt.Extensions.Xunit;
using Xunit;
namespace Contoso.Sales.Tests;
public class RevenueProjectionCacheTest : Test
{
public RevenueProjectionCacheTest(ITestOutputHelper output) : base(output)
{
}
[Fact]
public void ShouldReuseMemoizedProjection_WhenArgumentsRepeat()
{
var cache = new SlimMemoryCache();
var projectionsBuilt = 0;
var projectRevenue = cache.Memoize(TimeSpan.FromMinutes(5), (int days, decimal dailyRevenue) =>
{
projectionsBuilt++;
return days * dailyRevenue;
});
var monthlyProjection = projectRevenue(30, 125.50m);
var repeatedProjection = projectRevenue(30, 125.50m);
var weeklyProjection = projectRevenue(7, 125.50m);
Assert.Equal(3765.00m, monthlyProjection);
Assert.Equal(monthlyProjection, repeatedProjection);
Assert.Equal(878.50m, weeklyProjection);
Assert.Equal(2, projectionsBuilt);
Assert.Equal(2, cache.Count(CacheEnumerableExtensions.MemoizationScope));
TestOutput.WriteLine($"Memoized entries: {cache.Count(CacheEnumerableExtensions.MemoizationScope)}");
}
}
Use this pattern when an expensive in-process calculation is repeatedly called with the same arguments, such as a projection or normalization step that already has a defined cache lifetime. It matters because Memoize preserves the delegate-shaped calling code while moving cache key generation and invalidation into the cache layer.
Installation
dotnet add package Cuemon.Extensions.Runtime.Caching
Usage guidance
Use this package when you already work with an ICacheEnumerable<TKey> implementation and want read-through lookups or memoized delegates without scattering cache lookup and invalidation code throughout the application. If you only need a cache implementation, start with Cuemon.Runtime.Caching, and if you need a distributed or framework-owned cache abstraction, use that lower-level or platform-specific option instead of this extension layer.
Family packages
- 🌐Cuemon.AspNetCore
- 🏭Cuemon.AspNetCore.App
- 🌐Cuemon.AspNetCore.Authentication
- 🌐Cuemon.AspNetCore.Mvc
- 🌐Cuemon.AspNetCore.Razor.TagHelpers
- 📦Cuemon.Core
- 🏭Cuemon.Core.App
- 🗄️Cuemon.Data
- 🗄️Cuemon.Data.Integrity
- 🗄️Cuemon.Data.SqlClient
- 🩺Cuemon.Diagnostics
- 🌐Cuemon.Extensions.AspNetCore
- 🌐Cuemon.Extensions.AspNetCore.Authentication
- 🌐Cuemon.Extensions.AspNetCore.Mvc
- 🌐Cuemon.Extensions.AspNetCore.Mvc.Formatters.Text.Json
- 🌐Cuemon.Extensions.AspNetCore.Mvc.Formatters.Xml
- 🌐Cuemon.Extensions.AspNetCore.Mvc.RazorPages
- 🌐Cuemon.Extensions.AspNetCore.Text.Json
- 🌐Cuemon.Extensions.AspNetCore.Xml
- 📦Cuemon.Extensions.Collections.Generic
- 📦Cuemon.Extensions.Collections.Specialized
- 📦Cuemon.Extensions.Core
- 🗄️Cuemon.Extensions.Data
- 🗄️Cuemon.Extensions.Data.Integrity
- 📦Cuemon.Extensions.DependencyInjection
- 🩺Cuemon.Extensions.Diagnostics
- 🏗️Cuemon.Extensions.Hosting
- 📦Cuemon.Extensions.IO
- 📦Cuemon.Extensions.Net
- 📦Cuemon.Extensions.Reflection
- 📝Cuemon.Extensions.Text
- 📝Cuemon.Extensions.Text.Json
- 📦Cuemon.Extensions.Threading
- 📦Cuemon.Extensions.Xml
- 📦Cuemon.IO
- ⚙️Cuemon.Kernel
- 📦Cuemon.Net
- 📦Cuemon.Resilience
- 📦Cuemon.Runtime.Caching
- 🔐Cuemon.Security.Cryptography
- 📦Cuemon.Threading
- 📦Cuemon.Xml