C# CacheManager / CachedValue

Date: 2025-03-28
using System;
using System.Threading.Tasks;

namespace Domain.Caches
{
    public interface ICachedValue<T> : IDisposable
    {
        Task<T> GetValueAsync();
        void ClearValue();
        bool IsExpired();
    }

    public interface ICachedValueProvider
    {
        ICachedValue<T> CreateCachedValue<T>(
            string key,
            TimeSpan expiration,
            Func<Task<T>> valueFactory,
            TimeSpan? staleRevalidateWindow = null);
    }
}


////
using System;
using System.Threading.Tasks;
using CacheAdapter.Caches;
using Domain.Caches;
using Microsoft.Extensions.Caching.Memory;

namespace CacheAdapter
{
    public class CachedValueProvider : ICachedValueProvider
    {
        private readonly IMemoryCache _memoryCache;

        public CachedValueProvider(IMemoryCache memoryCache)
        {
            _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
        }

        public ICachedValue<T> CreateCachedValue<T>(string key, TimeSpan expiration, Func<Task<T>> valueFactory, TimeSpan? staleRevalidateWindow = null)
        {
            return new CachedValue<T>(
                cache: _memoryCache,
                key: key,
                expiration: expiration,
                valueFactory: valueFactory,
                staleRevalidateWindow: staleRevalidateWindow
            );
        }
    }
}


////
using System;
using System.Threading;
using System.Threading.Tasks;
using Domain.Caches;
using Microsoft.Extensions.Caching.Memory;

namespace CacheAdapter.Caches
{
    public class CachedValue<T> : ICachedValue<T>
    {
        private readonly IMemoryCache _cache;
        private readonly string _key;
        private readonly Func<Task<T>> _valueFactory;
        private readonly TimeSpan _expiration;
        private readonly TimeSpan _staleRevalidateWindow;
        private readonly SemaphoreSlim _semaphore = new(1, 1);

        private DateTime _lastUpdated = DateTime.MinValue;
        private bool _isRefreshing;

        public CachedValue(IMemoryCache cache, string key, TimeSpan expiration, Func<Task<T>> valueFactory, ILogger logger = null, TimeSpan? staleRevalidateWindow = null)
        {
            _cache = cache ?? throw new ArgumentNullException(nameof(cache));
            _key = key ?? throw new ArgumentNullException(nameof(key));
            _expiration = expiration;
            _staleRevalidateWindow = staleRevalidateWindow ?? TimeSpan.FromSeconds(30);
            _valueFactory = valueFactory ?? throw new ArgumentNullException(nameof(valueFactory));
        }

        public async Task<T> GetValueAsync()
        {
            // Probeer cached item op te halen
            if (_cache.TryGetValue(_key, out T value))
            {
                var age = DateTime.UtcNow - _lastUpdated;
                if (!_isRefreshing && age > _expiration - _staleRevalidateWindow)
                    TriggerBackgroundRefresh();
                return value;
            }

            // Geen waarde in cache — geforceerde refresh
            await _semaphore.WaitAsync();
            try
            {
                if (_cache.TryGetValue(_key, out value)) return value;
                value = await _valueFactory();
                SetCache(value);
                return value;
            }
            finally
            {
                _semaphore.Release();
            }
        }

        private void TriggerBackgroundRefresh()
        {
            if (_isRefreshing) return;

            _isRefreshing = true;
            _ = Task.Run(async () =>
            {
                try
                {
                    await _semaphore.WaitAsync();
                    var newValue = await _valueFactory();
                    SetCache(newValue);
                }
                catch (Exception ex)
                {
                    // ignore
                }
                finally
                {
                    _isRefreshing = false;
                    _semaphore.Release();
                }
            });
        }

        private void SetCache(T value)
        {
            _cache.Set(_key, value, new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = _expiration
            });
            _lastUpdated = DateTime.UtcNow;
        }

        public bool IsExpired()
        {
            if (_lastUpdated == DateTime.MinValue) return true;
            var elapsed = DateTime.UtcNow - _lastUpdated;
            return elapsed > _expiration;
        }

        public void ClearValue()
        {
            _cache.Remove(_key);
            _lastUpdated = DateTime.MinValue;
        }

        public void Dispose() => _semaphore.Dispose();
    }
}



Older:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;

public interface ICachedValue : IDisposable
{
    bool IsExpired();
}

public class CachedValue<T> : ICachedValue
{
    private readonly Func<Task<T>> _valueFactory;
    private readonly TimeSpan _expiration;
    private T _cachedValue;
    private DateTime _lastUpdated = DateTime.MinValue;
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    public CachedValue(TimeSpan expiration, Func<Task<T>> valueFactory)
    {
        _expiration = expiration;
        _valueFactory = valueFactory;
    }

    public async Task<T> GetValueAsync()
    {
        if (!IsExpired())
            return _cachedValue;

        await _semaphore.WaitAsync();
        try
        {
            if (IsExpired())
            {
                _cachedValue = await _valueFactory();
                _lastUpdated = DateTime.UtcNow;
            }
            return _cachedValue!;
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public void Reset() => _lastUpdated = DateTime.MinValue;

    public bool IsExpired() => DateTime.UtcNow - _lastUpdated > _expiration;
    
    public void Dispose()
    {
        _semaphore.Dispose();
    }
}

public class CacheManager
{
    private readonly ConcurrentDictionary<string, ICachedValue> _cache = new();
    private DateTime _lastCleanup = DateTime.MinValue;
    public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromMinutes(5);
    public DateTime LastCleanup => _lastCleanup;

    public async Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> valueFactory, TimeSpan expiration)
    {
        MaybeCleanup();

        var existing = _cache.GetOrAdd(key, _ =>
            new CachedValue<T>(expiration, valueFactory));

        if (existing is CachedValue<T> typedCached)
        {
            return await typedCached.GetValueAsync();
        }
        else
        {
            throw new InvalidOperationException($"Cached value for key '{key}' has an unexpected type. Expected: {typeof(T).Name}, Actual: {existing.GetType().Name}");
        }
    }

    public void Cleanup()
    {
        foreach (var kvp in _cache)
        {
            if (kvp.Value.IsExpired())
            {
                if (_cache.TryRemove(kvp.Key, out var removed))
                {
                    removed.Dispose();
                }
            }
        }
    
        _lastCleanup = DateTime.UtcNow;
    }

    private void MaybeCleanup()
    {
        var now = DateTime.UtcNow;
        if (now - _lastCleanup >= CleanupInterval)
        {
            Cleanup();
        }
    }

    public int Count => _cache.Count;
}

Example use:

var cache = new CacheManager();
string key = "user:42";

// Cache een string
string value = await cache.GetOrAddAsync(key, async () =>
{
    await Task.Delay(100);
    return "John Doe";
}, TimeSpan.FromMinutes(5));

// Cache een int onder een andere key
int age = await cache.GetOrAddAsync("user:42:age", async () =>
{
    await Task.Delay(50);
    return 30;
}, TimeSpan.FromMinutes(1));

Old:

using System;
using System.Threading;
using System.Threading.Tasks;

public class CachedValue<T>
{
    private readonly Func<Task<T>> _valueFactory;
    private readonly TimeSpan _expiration;
    private T _cachedValue;
    private DateTime _lastUpdated = DateTime.MinValue;
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    public CachedValue(TimeSpan expiration, Func<Task<T>> valueFactory)
    {
        _expiration = expiration;
        _valueFactory = valueFactory;
    }

    public async Task<T> GetValueAsync()
    {
        var cachedValue = _cachedValue;
        if (cachedValue != null && DateTime.UtcNow - _lastUpdated <= _expiration)
            return cachedValue;

        await _semaphore.WaitAsync();
        try
        {
            if (_cachedValue == null || DateTime.UtcNow - _lastUpdated > _expiration)
            {
                _cachedValue = await _valueFactory();
                _lastUpdated = DateTime.UtcNow;
            }
            return _cachedValue!;
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public void ResetAsync() => _lastUpdated = DateTime.MinValue;
}
private readonly CachedValue<int> _summedValue;

public MyClass()
{
    _summedValue = new CachedValue<int>(TimeSpan.FromMinutes(10), async () => await GetSummedValue());
}

public Task<int> GetSummedValueAsync() => _summedValue.GetValueAsync();

With MemoryCache

using System;
using System.Runtime.Caching;
using System.Threading;
using System.Threading.Tasks;

public interface ICachedValue : IDisposable
{
    bool IsExpired();
    void Reset();
}

public class CachedValue<T> : ICachedValue
{
    private static readonly MemoryCache Cache = MemoryCache.Default;

    private readonly string _key;
    private readonly Func<Task<T>> _valueFactory;
    private readonly TimeSpan _expiration;
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    private DateTime _lastUpdated = DateTime.MinValue;

    public CachedValue(string key, TimeSpan expiration, Func<Task<T>> valueFactory)
    {
        _key = key ?? throw new ArgumentNullException(nameof(key));
        _expiration = expiration;
        _valueFactory = valueFactory ?? throw new ArgumentNullException(nameof(valueFactory));
    }

    public async Task<T> GetValueAsync()
    {
        if (Cache.Get(_key) is T cached && !IsExpired())
            return cached;

        await _semaphore.WaitAsync();
        try
        {
            if (Cache.Get(_key) is T stillCached && !IsExpired())
                return stillCached;

            // Haal nieuwe waarde op
            var newValue = await _valueFactory();

            // Plaats in MemoryCache
            var policy = new CacheItemPolicy
            {
                AbsoluteExpiration = DateTimeOffset.UtcNow.Add(_expiration)
            };

            Cache.Set(_key, newValue, policy);
            _lastUpdated = DateTime.UtcNow;

            return newValue;
        }
        finally
        {
            _semaphore.Release();
        }
    }

    public void Reset()
    {
        Cache.Remove(_key);
        _lastUpdated = DateTime.MinValue;
    }

    public bool IsExpired()
    {
        if (_lastUpdated == DateTime.MinValue) return true;
        return DateTime.UtcNow - _lastUpdated > _expiration;
    }

    public void Dispose() => _semaphore.Dispose();
}
93710cookie-checkC# CacheManager / CachedValue