C# CacheManager / CachedValue

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


public interface ICachedValue : IDisposable
{
    bool IsExpired();

    void ClearValue();
}

public interface ICachedValue<T> : ICachedValue, IDisposable
{
    Task<T> GetValueAsync();
}

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

public interface ICachedValueCacheManager
{
    Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> valueFactory, TimeSpan expiration, TimeSpan? staleRevalidateWindow = null);

    void Cleanup();
}


// implementation

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

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

    public CachedValue(TimeSpan expiration, Func<Task<T>> valueFactory, TimeSpan? staleRevalidateWindow = null)
    {
        _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, gebruik currentValue zodat _cachedValue tussentijds null gezet kan worden
        var currentValue = _cachedValue;
        if (currentValue != null)
        {
            var age = DateTime.UtcNow - _lastUpdated;
            if (!_isRefreshing && age > _expiration - _staleRevalidateWindow)
                TriggerBackgroundRefresh();
            return currentValue;
        }

        // Geen waarde in cache — geforceerde refresh
        await _semaphore.WaitAsync();
        try
        {
            if (_cachedValue != null) return _cachedValue;
            var value = await _valueFactory();
            SetCache(value);
            return _cachedValue;
        }
        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)
            {
                // ignore: exception should be handled in valueFactory
            }
            finally
            {
                _isRefreshing = false;
                _semaphore.Release();
            }
        });
    }

    private void SetCache(T value)
    {
        _cachedValue = value;
        _lastUpdated = DateTime.UtcNow;
    }

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

    public void ClearValue()
    {
        _cachedValue = default;
        _lastUpdated = DateTime.MinValue;
    }

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

public class CachedValueProvider : ICachedValueProvider
{
    public ICachedValue<T> CreateCachedValue<T>(TimeSpan expiration, Func<Task<T>> valueFactory, TimeSpan? staleRevalidateWindow = null)
    {
        return new CachedValue<T>(expiration, valueFactory, staleRevalidateWindow);
    }
}

public class CachedValueCacheManager : ICachedValueCacheManager
{
    private readonly ICachedValueProvider cachedValueProvider;
    private readonly TimeSpan cleanupInterval;

    public CachedValueCacheManager(ICachedValueProvider cachedValueProvider, TimeSpan cleanupInterval)
    {
        this.cachedValueProvider = cachedValueProvider;
        this.cleanupInterval = cleanupInterval;
    }
    private readonly ConcurrentDictionary<string, ICachedValue> _cache = new ConcurrentDictionary<string, ICachedValue>();
    private DateTime _lastCleanup = DateTime.MinValue;
    public DateTime LastCleanup => _lastCleanup;

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

        var existing = _cache.GetOrAdd(key, _ => cachedValueProvider.CreateCachedValue<T>(expiration, valueFactory, staleRevalidateWindow));

        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()) continue;
            if (_cache.TryRemove(kvp.Key, out var removed))
            {
                removed.Dispose();
            }
        }
        _lastCleanup = DateTime.UtcNow;
    }

    private void MaybeCleanup()
    {
        if (DateTime.UtcNow - _lastCleanup < cleanupInterval) return;
        Cleanup();
    }

    public int Count => _cache.Count;
}

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