C# CacheManager / CachedValue

Date: 2025-03-28
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();
93710cookie-checkC# CacheManager / CachedValue