< Summary

Information
Class: MyNet.Utilities.Caching.CacheStorage<T1, T2>
Assembly: MyNet.Utilities
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Utilities/Caching/CacheStorage.cs
Tag: 323_28699572109
Line coverage
76%
Covered lines: 86
Uncovered lines: 26
Coverable lines: 112
Total lines: 412
Line coverage: 76.7%
Branch coverage
65%
Covered branches: 42
Total branches: 64
Branch coverage: 65.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
get_Keys()100%210%
get_ExpirationTimerInterval()100%210%
set_ExpirationTimerInterval(...)75%4485.71%
get_Item(...)100%11100%
Get(...)100%22100%
Contains(...)100%11100%
GetFromCacheOrFetch(...)75%121291.66%
GetFromCacheOrFetch(...)100%11100%
Add(...)100%11100%
Add(...)100%11100%
Remove(...)100%1125%
Clear()0%620%
GetKeysSnapshot()100%210%
RemoveExpiredItems()87.5%8891.66%
OnTimerElapsed(...)100%11100%
TryGetValueInfoUnsafe(...)100%44100%
RemoveItemUnsafe(...)40%262076%
UpdateExpirationStateUnsafe()100%44100%
UpdateTimerUnsafe()66.66%66100%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Utilities/Caching/CacheStorage.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="CacheStorage.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Generic;
 9using System.Linq;
 10using System.Threading;
 11using MyNet.Utilities.Caching.Policies;
 12
 13namespace MyNet.Utilities.Caching;
 14
 15/// <summary>
 16/// The cache storage.
 17/// </summary>
 18/// <typeparam name="TKey">The key type.</typeparam>
 19/// <typeparam name="TValue">The value type.</typeparam>
 20/// <remarks>
 21/// Initializes a new instance of the <see cref="CacheStorage{TKey,TValue}" /> class.
 22/// </remarks>
 23/// <param name="defaultExpirationPolicyInitCode">The default expiration policy initialization code.</param>
 24/// <param name="storeNullValues">Allow store null values on the cache.</param>
 25/// <param name="equalityComparer">The equality comparer.</param>
 26public sealed class CacheStorage<TKey, TValue>(Func<ExpirationPolicy>? defaultExpirationPolicyInitCode = null, bool stor
 27    IEqualityComparer<TKey>? equalityComparer = null) : ICacheStorage<TKey, TValue>
 28    where TKey : notnull
 29{
 30    #region Fields
 31
 32    /// <summary>
 33    /// Determines whether the cache storage can store null values.
 34    /// </summary>
 1835    private readonly bool _storeNullValues = storeNullValues;
 36
 37    /// <summary>
 38    /// The dictionary.
 39    /// </summary>
 1840    private readonly Dictionary<TKey, CacheStorageValueInfo<TValue>> _dictionary = new(equalityComparer ?? EqualityCompa
 41
 42    /// <summary>
 43    /// The synchronization object.
 44    /// </summary>
 1845    private readonly Lock _syncObj = new();
 46
 47    /// <summary>
 48    /// The timer that is being executed to invalidate the cache.
 49    /// </summary>
 50    private Timer? _expirationTimer;
 51
 52    /// <summary>
 53    /// Determines whether the cache storage can check for expired items.
 54    /// </summary>
 55    private bool _checkForExpiredItems;
 56
 57    #endregion
 58
 59    /// <summary>
 60    /// Occurs when the item is expiring.
 61    /// </summary>
 62    public event EventHandler<ExpiringEventArgs<TKey, TValue>>? Expiring;
 63
 64    /// <summary>
 65    /// Occurs when the item has expired.
 66    /// </summary>
 67    public event EventHandler<ExpiredEventArgs<TKey, TValue>>? Expired;
 68
 69    #region ICacheStorage<TKey,TValue> Members
 70
 71    /// <summary>
 72    /// Gets or sets a value indicating whether values should be disposed on removal.
 73    /// </summary>
 74    /// <value><c>true</c> if values should be disposed on removal; otherwise, <c>false</c>.</value>
 75    public bool DisposeValuesOnRemoval { get; set; }
 76
 77    /// <summary>
 78    /// Gets the keys so it is possible to enumerate the cache.
 79    /// </summary>
 80    /// <value>The keys.</value>
 081    public IEnumerable<TKey> Keys => GetKeysSnapshot();
 82
 83    /// <summary>
 84    /// Gets or sets the expiration timer interval.
 85    /// <para />
 86    /// The default value is <c>TimeSpan.FromSeconds(1)</c>.
 87    /// </summary>
 88    /// <value>The expiration timer interval.</value>
 89    public TimeSpan ExpirationTimerInterval
 90    {
 91        get
 092        {
 93            lock (_syncObj)
 94            {
 095                return _expirationTimerInterval;
 96            }
 097        }
 98
 99        set
 100        {
 9101            if (value <= TimeSpan.Zero)
 102            {
 6103                throw new ArgumentOutOfRangeException(nameof(value), value, "Expiration timer interval must be greater t
 104            }
 105
 106            lock (_syncObj)
 107            {
 3108                _expirationTimerInterval = value;
 3109                if (_checkForExpiredItems)
 110                {
 0111                    UpdateTimerUnsafe();
 112                }
 3113            }
 3114        }
 115    }
 116
 18117    private TimeSpan _expirationTimerInterval = TimeSpan.FromSeconds(1);
 118
 119    /// <summary>
 120    /// Gets the value associated with the specified key.
 121    /// </summary>
 122    /// <param name="key">The key.</param>
 123    /// <returns>The value associated with the specified key, or default value for the type of the value if the key do n
 124    /// <exception cref="ArgumentNullException">The <paramref name="key" /> is <c>null</c>.</exception>
 6125    public TValue? this[TKey key] => Get(key);
 126
 127    /// <summary>
 128    /// Gets the value associated with the specified <paramref name="key"/>.
 129    /// </summary>
 130    /// <param name="key">The key of the value to get.</param>
 131    /// <returns>The value associated with the specified key, or default value for the type of the value if the key do n
 132    /// <exception cref="ArgumentNullException">The <paramref name="key" /> is <c>null</c>.</exception>
 133    public TValue? Get(TKey key)
 134    {
 12135        ArgumentNullException.ThrowIfNull(key);
 136
 137        lock (_syncObj)
 138        {
 9139            return !TryGetValueInfoUnsafe(key, out var valueInfo) ? default : valueInfo.Value;
 140        }
 9141    }
 142
 143    /// <summary>
 144    /// Determines whether the cache contains a value associated with the specified key.
 145    /// </summary>
 146    /// <param name="key">The key.</param>
 147    /// <returns><c>true</c> if the cache contains an element with the specified key; otherwise, <c>false</c>.</returns>
 148    /// <exception cref="ArgumentNullException">The <paramref name="key" /> is <c>null</c>.</exception>
 149    public bool Contains(TKey key)
 150    {
 9151        ArgumentNullException.ThrowIfNull(key);
 152
 153        lock (_syncObj)
 154        {
 6155            return TryGetValueInfoUnsafe(key, out _);
 156        }
 6157    }
 158
 159    /// <summary>
 160    /// Adds a value to the cache associated with to a key.
 161    /// </summary>
 162    /// <param name="key">The key.</param>
 163    /// <param name="code">The deferred initialization code of the value.</param>
 164    /// <param name="expirationPolicy">The expiration policy.</param>
 165    /// <param name="override">Indicates if the key exists the value will be overridden.</param>
 166    /// <returns>The instance initialized by the <paramref name="code" />.</returns>
 167    /// <exception cref="ArgumentNullException">If <paramref name="key" /> is <c>null</c>.</exception>
 168    /// <exception cref="ArgumentNullException">If <paramref name="code" /> is <c>null</c>.</exception>
 169    public TValue GetFromCacheOrFetch(TKey key, Func<TValue> code, ExpirationPolicy? expirationPolicy, bool @override = 
 170    {
 18171        ArgumentNullException.ThrowIfNull(key);
 15172        ArgumentNullException.ThrowIfNull(code);
 173
 174        lock (_syncObj)
 175        {
 15176            if (!@override && TryGetValueInfoUnsafe(key, out var cacheStorageValueInfo))
 177            {
 3178                return cacheStorageValueInfo.Value;
 179            }
 180
 12181            var value = code();
 12182            if (value is null && !_storeNullValues)
 183            {
 0184                return value;
 185            }
 186
 12187            expirationPolicy ??= defaultExpirationPolicyInitCode?.Invoke();
 188
 12189            _dictionary[key] = new(value, expirationPolicy);
 12190            UpdateExpirationStateUnsafe();
 191
 12192            return value;
 193        }
 15194    }
 195
 196    /// <summary>
 197    /// Adds a value to the cache associated with to a key.
 198    /// </summary>
 199    /// <param name="key">The key.</param>
 200    /// <param name="code">The deferred initialization code of the value.</param>
 201    /// <param name="override">Indicates if the key exists the value will be overridden.</param>
 202    /// <param name="expiration">The timespan in which the cache item should expire when added.</param>
 203    /// <returns>The instance initialized by the <paramref name="code" />.</returns>
 204    /// <exception cref="ArgumentNullException">If <paramref name="key" /> is <c>null</c>.</exception>
 205    /// <exception cref="ArgumentNullException">If <paramref name="code" /> is <c>null</c>.</exception>
 9206    public TValue GetFromCacheOrFetch(TKey key, Func<TValue> code, bool @override = false, TimeSpan expiration = default
 207
 208    /// <summary>
 209    /// Adds a value to the cache associated with to a key.
 210    /// </summary>
 211    /// <param name="key">The key.</param>
 212    /// <param name="value">The value.</param>
 213    /// <param name="override">Indicates if the key exists the value will be overridden.</param>
 214    /// <param name="expiration">The timespan in which the cache item should expire when added.</param>
 215    /// <exception cref="ArgumentNullException">The <paramref name="key" /> is <c>null</c>.</exception>
 6216    public void Add(TKey key, TValue value, bool @override = false, TimeSpan expiration = default) => Add(key, value, Ex
 217
 218    /// <summary>
 219    /// Adds a value to the cache associated with to a key.
 220    /// </summary>
 221    /// <param name="key">The key.</param>
 222    /// <param name="value">The value.</param>
 223    /// <param name="expirationPolicy">The expiration policy.</param>
 224    /// <param name="override">Indicates if the key exists the value will be overridden.</param>
 225    /// <exception cref="ArgumentNullException">The <paramref name="key" /> is <c>null</c>.</exception>
 6226    public void Add(TKey key, TValue value, ExpirationPolicy? expirationPolicy, bool @override = false) => GetFromCacheO
 227
 228    /// <summary>
 229    /// Removes an item from the cache.
 230    /// </summary>
 231    /// <param name="key">The key.</param>
 232    /// <param name="action">The action that need to be executed in synchronization with the item cache removal.</param>
 233    /// <exception cref="ArgumentNullException">The <paramref name="key" /> is <c>null</c>.</exception>
 234    public void Remove(TKey key, Action? action = null)
 235    {
 3236        ArgumentNullException.ThrowIfNull(key);
 237
 238        lock (_syncObj)
 239        {
 0240            _ = RemoveItemUnsafe(key, false, action);
 0241        }
 0242    }
 243
 244    /// <summary>
 245    /// Clears all the items currently in the cache.
 246    /// </summary>
 247    public void Clear()
 0248    {
 249        lock (_syncObj)
 250        {
 0251            var keysToRemove = _dictionary.Keys.ToList();
 0252            foreach (var keyToRemove in keysToRemove)
 253            {
 0254                _ = RemoveItemUnsafe(keyToRemove, false);
 255            }
 256
 0257            UpdateExpirationStateUnsafe();
 0258        }
 0259    }
 260
 261    private TKey[] GetKeysSnapshot()
 0262    {
 263        lock (_syncObj)
 264        {
 0265            return [.. _dictionary.Keys];
 266        }
 0267    }
 268
 269    /// <summary>
 270    /// Removes the expired items from the cache.
 271    /// </summary>
 272    private void RemoveExpiredItems()
 3273    {
 274        lock (_syncObj)
 275        {
 3276            if (!_checkForExpiredItems)
 277            {
 0278                return;
 279            }
 280
 3281            var keysToRemove = new List<TKey>();
 12282            foreach (var (key, valueInfo) in _dictionary)
 283            {
 3284                if (valueInfo.IsExpired)
 285                {
 3286                    keysToRemove.Add(key);
 287                }
 288            }
 289
 12290            foreach (var keyToRemove in keysToRemove)
 291            {
 3292                _ = RemoveItemUnsafe(keyToRemove, true);
 293            }
 294
 3295            UpdateExpirationStateUnsafe();
 3296        }
 3297    }
 298
 299    /// <summary>
 300    /// Called when the timer to clean up the cache elapsed.
 301    /// </summary>
 302    /// <param name="state">The timer state.</param>
 3303    private void OnTimerElapsed(object? state) => RemoveExpiredItems();
 304
 305    private bool TryGetValueInfoUnsafe(TKey key, out CacheStorageValueInfo<TValue> valueInfo)
 306    {
 30307        if (!_dictionary.TryGetValue(key, out var cacheStorageValueInfo))
 308        {
 18309            valueInfo = null!;
 18310            return false;
 311        }
 312
 12313        valueInfo = cacheStorageValueInfo;
 314
 12315        if (!valueInfo.IsExpired)
 316        {
 9317            return true;
 318        }
 319
 3320        _ = RemoveItemUnsafe(key, true);
 321
 3322        valueInfo = null!;
 3323        return false;
 324    }
 325
 326    /// <summary>
 327    /// Remove item from cache by key.
 328    /// </summary>
 329    /// <param name="key">The key.</param>
 330    /// <param name="raiseEvents">Indicates whether events should be raised.</param>
 331    /// <param name="action">The action that need to be executed in synchronization with the item cache removal.</param>
 332    /// <returns>The value indicating whether the item was removed.</returns>
 333    private bool RemoveItemUnsafe(TKey key, bool raiseEvents, Action? action = null)
 334    {
 335        // Try to get item, if there is no item by that key then return true to indicate that item was removed.
 6336        if (!_dictionary.TryGetValue(key, out var item))
 337        {
 0338            return true;
 339        }
 340
 6341        action?.Invoke();
 342
 6343        var cancel = false;
 6344        var expirationPolicy = item.ExpirationPolicy;
 6345        if (raiseEvents)
 346        {
 6347            var expiringEventArgs = new ExpiringEventArgs<TKey, TValue>(key, item.Value, expirationPolicy);
 6348            Expiring?.Invoke(this, expiringEventArgs);
 349
 6350            cancel = expiringEventArgs.Cancel;
 6351            expirationPolicy = expiringEventArgs.ExpirationPolicy;
 352        }
 353
 6354        if (cancel)
 355        {
 0356            expirationPolicy ??= defaultExpirationPolicyInitCode?.Invoke();
 357
 0358            _dictionary[key] = new(item.Value, expirationPolicy);
 0359            UpdateExpirationStateUnsafe();
 360
 0361            return false;
 362        }
 363
 6364        _ = _dictionary.Remove(key);
 365
 6366        var dispose = DisposeValuesOnRemoval;
 6367        if (raiseEvents)
 368        {
 6369            var expiredEventArgs = new ExpiredEventArgs<TKey, TValue>(key, item.Value, dispose);
 6370            Expired?.Invoke(this, expiredEventArgs);
 371
 6372            dispose = expiredEventArgs.Dispose;
 373        }
 374
 6375        if (dispose)
 376        {
 0377            item.DisposeValue();
 378        }
 379
 6380        UpdateExpirationStateUnsafe();
 381
 6382        return true;
 383    }
 384
 385    private void UpdateExpirationStateUnsafe()
 386    {
 21387        var containsItemsThatCanExpire = _dictionary.Values.Any(x => x.CanExpire);
 21388        if (_checkForExpiredItems == containsItemsThatCanExpire)
 389        {
 9390            return;
 391        }
 392
 12393        _checkForExpiredItems = containsItemsThatCanExpire;
 12394        UpdateTimerUnsafe();
 12395    }
 396
 397    private void UpdateTimerUnsafe()
 398    {
 12399        if (!_checkForExpiredItems)
 400        {
 6401            _expirationTimer?.Dispose();
 6402            _expirationTimer = null;
 6403            return;
 404        }
 405
 6406        var timeSpan = _expirationTimerInterval;
 6407        _expirationTimer ??= new(OnTimerElapsed, null, Timeout.Infinite, Timeout.Infinite);
 6408        _ = _expirationTimer.Change(timeSpan, timeSpan);
 6409    }
 410    #endregion
 411}
 412