{"id":9371,"date":"2025-03-28T10:23:14","date_gmt":"2025-03-28T09:23:14","guid":{"rendered":"https:\/\/solidt.eu\/site\/?p=9371"},"modified":"2025-12-24T14:13:53","modified_gmt":"2025-12-24T13:13:53","slug":"c-cachedvalue","status":"publish","type":"post","link":"https:\/\/solidt.eu\/site\/c-cachedvalue\/","title":{"rendered":"C# CacheManager \/ CachedValue"},"content":{"rendered":"\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"csharp\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">using System;\nusing System.Collections.Concurrent;\nusing System.Threading.Tasks;\n\n\npublic interface ICachedValue : IDisposable\n{\n    bool IsExpired();\n\n    void ClearValue();\n}\n\npublic interface ICachedValue&lt;T> : ICachedValue, IDisposable\n{\n    T GetValue();\n    Task&lt;T> GetValueAsync();\n}\n\npublic interface ICachedValueProvider\n{\n    ICachedValue&lt;T> CreateCachedValue&lt;T>(TimeSpan expiration, Func&lt;Task&lt;T>> valueFactory, TimeSpan? staleRevalidateWindow = null);\n}\n\npublic interface ICachedValueCacheManager\n{\n    Task&lt;T> GetOrAddAsync&lt;T>(string key, Func&lt;Task&lt;T>> valueFactory, TimeSpan expiration, TimeSpan? staleRevalidateWindow = null);\n\n    void Cleanup();\n}\n\n\n\/\/ implementation\n\npublic class CachedValue&lt;T> : ICachedValue&lt;T>\n{\n    private T _cachedValue;\n    private readonly Func&lt;Task&lt;T>> _valueFactory;\n    private readonly TimeSpan _expiration;\n    private readonly TimeSpan _staleRevalidateWindow;\n    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);\n\n    private DateTime _lastUpdated = DateTime.MinValue;\n    private volatile bool _isRefreshing;\n\n    public CachedValue(\n        TimeSpan expiration,\n        Func&lt;Task&lt;T>> valueFactory,\n        TimeSpan? staleRevalidateWindow = null)\n    {\n        _expiration = expiration;\n        _staleRevalidateWindow = staleRevalidateWindow ?? TimeSpan.FromSeconds(30);\n        _valueFactory = valueFactory ?? throw new ArgumentNullException(nameof(valueFactory));\n    }\n\n    public async Task&lt;T> GetValueAsync()\n    {\n        var currentValue = _cachedValue;\n        if (currentValue != null)\n        {\n            if (IsStaleOrExpired())\n                TriggerBackgroundRefresh();\n\n            return currentValue;\n        }\n\n        \/\/ Geen waarde \u2192 foreground refresh\n        return await RefreshAsync();\n    }\n\n    public T GetValue()\n    {\n        var currentValue = _cachedValue;\n\n        if (currentValue != null &amp;&amp; IsStaleOrExpired())\n            TriggerBackgroundRefresh();\n\n        return currentValue;\n    }\n\n    private bool IsStaleOrExpired()\n    {\n        if (_lastUpdated == DateTime.MinValue)\n            return true;\n\n        var age = DateTime.UtcNow - _lastUpdated;\n        return age >= (_expiration - _staleRevalidateWindow);\n    }\n\n    private void TriggerBackgroundRefresh()\n    {\n        if (_isRefreshing)\n            return;\n\n        _isRefreshing = true;\n\n        _ = Task.Run(async () =>\n        {\n            try\n            {\n                await RefreshAsync();\n            }\n            catch\n            {\n                \/\/ bewust negeren\n            }\n            finally\n            {\n                _isRefreshing = false;\n            }\n        });\n    }\n\n    private async Task&lt;T> RefreshAsync()\n    {\n        await _semaphore.WaitAsync();\n        try\n        {\n            \/\/ Double-check na lock\n            if (_cachedValue != null &amp;&amp; !IsExpired())\n                return _cachedValue;\n\n            var value = await _valueFactory();\n            SetCache(value);\n            return value;\n        }\n        finally\n        {\n            _semaphore.Release();\n        }\n    }\n\n    private void SetCache(T value)\n    {\n        _cachedValue = value;\n        _lastUpdated = DateTime.UtcNow;\n    }\n\n    public bool IsExpired()\n    {\n        if (_lastUpdated == DateTime.MinValue)\n            return true;\n\n        return DateTime.UtcNow - _lastUpdated > _expiration;\n    }\n\n    public void ClearValue()\n    {\n        _cachedValue = default;\n        _lastUpdated = DateTime.MinValue;\n    }\n\n    public void Dispose()\n    {\n        _semaphore.Dispose();\n    }\n}<\/pre><\/div>\n\n\n\n<p>Older:<\/p>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"csharp\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">using System;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.Collections.Concurrent;\n\npublic interface ICachedValue : IDisposable\n{\n    bool IsExpired();\n}\n\npublic class CachedValue&lt;T> : ICachedValue\n{\n    private readonly Func&lt;Task&lt;T>> _valueFactory;\n    private readonly TimeSpan _expiration;\n    private T _cachedValue;\n    private DateTime _lastUpdated = DateTime.MinValue;\n    private readonly SemaphoreSlim _semaphore = new(1, 1);\n\n    public CachedValue(TimeSpan expiration, Func&lt;Task&lt;T>> valueFactory)\n    {\n        _expiration = expiration;\n        _valueFactory = valueFactory;\n    }\n\n    public async Task&lt;T> GetValueAsync()\n    {\n        if (!IsExpired())\n            return _cachedValue;\n\n        await _semaphore.WaitAsync();\n        try\n        {\n            if (IsExpired())\n            {\n                _cachedValue = await _valueFactory();\n                _lastUpdated = DateTime.UtcNow;\n            }\n            return _cachedValue!;\n        }\n        finally\n        {\n            _semaphore.Release();\n        }\n    }\n\n    public void Reset() => _lastUpdated = DateTime.MinValue;\n\n    public bool IsExpired() => DateTime.UtcNow - _lastUpdated > _expiration;\n    \n    public void Dispose()\n    {\n        _semaphore.Dispose();\n    }\n}\n\npublic class CacheManager\n{\n    private readonly ConcurrentDictionary&lt;string, ICachedValue> _cache = new();\n    private DateTime _lastCleanup = DateTime.MinValue;\n    public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromMinutes(5);\n    public DateTime LastCleanup => _lastCleanup;\n\n    public async Task&lt;T> GetOrAddAsync&lt;T>(string key, Func&lt;Task&lt;T>> valueFactory, TimeSpan expiration)\n    {\n        MaybeCleanup();\n\n        var existing = _cache.GetOrAdd(key, _ =>\n            new CachedValue&lt;T>(expiration, valueFactory));\n\n        if (existing is CachedValue&lt;T> typedCached)\n        {\n            return await typedCached.GetValueAsync();\n        }\n        else\n        {\n            throw new InvalidOperationException($\"Cached value for key '{key}' has an unexpected type. Expected: {typeof(T).Name}, Actual: {existing.GetType().Name}\");\n        }\n    }\n\n    public void Cleanup()\n    {\n        foreach (var kvp in _cache)\n        {\n            if (kvp.Value.IsExpired())\n            {\n                if (_cache.TryRemove(kvp.Key, out var removed))\n                {\n                    removed.Dispose();\n                }\n            }\n        }\n    \n        _lastCleanup = DateTime.UtcNow;\n    }\n\n    private void MaybeCleanup()\n    {\n        var now = DateTime.UtcNow;\n        if (now - _lastCleanup >= CleanupInterval)\n        {\n            Cleanup();\n        }\n    }\n\n    public int Count => _cache.Count;\n}\n\n<\/pre><\/div>\n\n\n\n<p>Example use:<\/p>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"csharp\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">var cache = new CacheManager();\nstring key = \"user:42\";\n\n\/\/ Cache een string\nstring value = await cache.GetOrAddAsync(key, async () =>\n{\n    await Task.Delay(100);\n    return \"John Doe\";\n}, TimeSpan.FromMinutes(5));\n\n\/\/ Cache een int onder een andere key\nint age = await cache.GetOrAddAsync(\"user:42:age\", async () =>\n{\n    await Task.Delay(50);\n    return 30;\n}, TimeSpan.FromMinutes(1));\n<\/pre><\/div>\n\n\n\n<p>Old:<\/p>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"csharp\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">using System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\npublic class CachedValue&lt;T>\n{\n    private readonly Func&lt;Task&lt;T>> _valueFactory;\n    private readonly TimeSpan _expiration;\n    private T _cachedValue;\n    private DateTime _lastUpdated = DateTime.MinValue;\n    private readonly SemaphoreSlim _semaphore = new(1, 1);\n\n    public CachedValue(TimeSpan expiration, Func&lt;Task&lt;T>> valueFactory)\n    {\n        _expiration = expiration;\n        _valueFactory = valueFactory;\n    }\n\n    public async Task&lt;T> GetValueAsync()\n    {\n        var cachedValue = _cachedValue;\n        if (cachedValue != null &amp;&amp; DateTime.UtcNow - _lastUpdated &lt;= _expiration)\n            return cachedValue;\n\n        await _semaphore.WaitAsync();\n        try\n        {\n            if (_cachedValue == null || DateTime.UtcNow - _lastUpdated > _expiration)\n            {\n                _cachedValue = await _valueFactory();\n                _lastUpdated = DateTime.UtcNow;\n            }\n            return _cachedValue!;\n        }\n        finally\n        {\n            _semaphore.Release();\n        }\n    }\n\n    public void ResetAsync() => _lastUpdated = DateTime.MinValue;\n}<\/pre><\/div>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"csharp\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">private readonly CachedValue&lt;int> _summedValue;\n\npublic MyClass()\n{\n    _summedValue = new CachedValue&lt;int>(TimeSpan.FromMinutes(10), async () => await GetSummedValue());\n}\n\npublic Task&lt;int> GetSummedValueAsync() => _summedValue.GetValueAsync();<\/pre><\/div>\n\n\n\n<p>With MemoryCache<\/p>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"csharp\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">using System;\nusing System.Runtime.Caching;\nusing System.Threading;\nusing System.Threading.Tasks;\n\npublic interface ICachedValue : IDisposable\n{\n    bool IsExpired();\n    void Reset();\n}\n\npublic class CachedValue&lt;T> : ICachedValue\n{\n    private static readonly MemoryCache Cache = MemoryCache.Default;\n\n    private readonly string _key;\n    private readonly Func&lt;Task&lt;T>> _valueFactory;\n    private readonly TimeSpan _expiration;\n    private readonly SemaphoreSlim _semaphore = new(1, 1);\n\n    private DateTime _lastUpdated = DateTime.MinValue;\n\n    public CachedValue(string key, TimeSpan expiration, Func&lt;Task&lt;T>> valueFactory)\n    {\n        _key = key ?? throw new ArgumentNullException(nameof(key));\n        _expiration = expiration;\n        _valueFactory = valueFactory ?? throw new ArgumentNullException(nameof(valueFactory));\n    }\n\n    public async Task&lt;T> GetValueAsync()\n    {\n        if (Cache.Get(_key) is T cached &amp;&amp; !IsExpired())\n            return cached;\n\n        await _semaphore.WaitAsync();\n        try\n        {\n            if (Cache.Get(_key) is T stillCached &amp;&amp; !IsExpired())\n                return stillCached;\n\n            \/\/ Haal nieuwe waarde op\n            var newValue = await _valueFactory();\n\n            \/\/ Plaats in MemoryCache\n            var policy = new CacheItemPolicy\n            {\n                AbsoluteExpiration = DateTimeOffset.UtcNow.Add(_expiration)\n            };\n\n            Cache.Set(_key, newValue, policy);\n            _lastUpdated = DateTime.UtcNow;\n\n            return newValue;\n        }\n        finally\n        {\n            _semaphore.Release();\n        }\n    }\n\n    public void Reset()\n    {\n        Cache.Remove(_key);\n        _lastUpdated = DateTime.MinValue;\n    }\n\n    public bool IsExpired()\n    {\n        if (_lastUpdated == DateTime.MinValue) return true;\n        return DateTime.UtcNow - _lastUpdated > _expiration;\n    }\n\n    public void Dispose() => _semaphore.Dispose();\n}\n<\/pre><\/div>\n","protected":false},"excerpt":{"rendered":"<p>Older: Example use: Old: With MemoryCache<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-9371","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/9371","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/comments?post=9371"}],"version-history":[{"count":13,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/9371\/revisions"}],"predecessor-version":[{"id":10003,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/9371\/revisions\/10003"}],"wp:attachment":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/media?parent=9371"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/categories?post=9371"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/tags?post=9371"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}