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
{
    T GetValue();
    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 volatile 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()
    {
        var currentValue = _cachedValue;
        if (currentValue != null)
        {
            if (IsStaleOrExpired())
                TriggerBackgroundRefresh();

            return currentValue;
        }

        // Geen waarde → foreground refresh
        return await RefreshAsync();
    }

    public T GetValue()
    {
        var currentValue = _cachedValue;

        if (currentValue != null && IsStaleOrExpired())
            TriggerBackgroundRefresh();

        return currentValue;
    }

    private bool IsStaleOrExpired()
    {
        if (_lastUpdated == DateTime.MinValue)
            return true;

        var age = DateTime.UtcNow - _lastUpdated;
        return age >= (_expiration - _staleRevalidateWindow);
    }

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

        _isRefreshing = true;

        _ = Task.Run(async () =>
        {
            try
            {
                await RefreshAsync();
            }
            catch
            {
                // bewust negeren
            }
            finally
            {
                _isRefreshing = false;
            }
        });
    }

    private async Task<T> RefreshAsync()
    {
        await _semaphore.WaitAsync();
        try
        {
            // Double-check na lock
            if (_cachedValue != null && !IsExpired())
                return _cachedValue;

            var value = await _valueFactory();
            SetCache(value);
            return value;
        }
        finally
        {
            _semaphore.Release();
        }
    }

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

    public bool IsExpired()
    {
        if (_lastUpdated == DateTime.MinValue)
            return true;

        return DateTime.UtcNow - _lastUpdated > _expiration;
    }

    public void ClearValue()
    {
        _cachedValue = default;
        _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