1
+ using System . Collections . Concurrent ;
2
+ using System . Linq . Dynamic . Core . Validation ;
3
+ using System . Threading . Tasks ;
4
+
5
+ namespace System . Linq . Dynamic . Core . Util
6
+ {
7
+ internal class ThreadSafeSlidingCache < TKey , TValue > where TKey : notnull where TValue : notnull
8
+ {
9
+ // ReSharper disable once StaticMemberInGenericType
10
+ private static readonly TimeSpan _defaultCleanupFrequency = TimeSpan . FromMinutes ( 10 ) ;
11
+ private readonly ConcurrentDictionary < TKey , ( TValue Value , DateTime ExpirationTime ) > _cache ;
12
+ private readonly TimeSpan _cleanupFrequency ;
13
+ private readonly IDateTimeUtils _dateTimeProvider ;
14
+ private readonly Func < Task > _deleteExpiredCachedItemsDelegate ;
15
+ private readonly long ? _minCacheItemsBeforeCleanup ;
16
+ private DateTime _lastCleanupTime = DateTime . MinValue ;
17
+
18
+ /// <summary>
19
+ /// Sliding Thread Safe Cache
20
+ /// </summary>
21
+ /// <param name="timeToLive">The length of time any object would survive before being removed</param>
22
+ /// <param name="cleanupFrequency">Only look for expired objects over specific periods</param>
23
+ /// <param name="minCacheItemsBeforeCleanup">
24
+ /// If defined, only allow the cleanup process after x number of cached items have
25
+ /// been stored
26
+ /// </param>
27
+ /// <param name="dateTimeProvider">
28
+ /// Provides the Time for the Caching object. Default will be created if not supplied. Used
29
+ /// for Testing classes
30
+ /// </param>
31
+ public ThreadSafeSlidingCache (
32
+ TimeSpan timeToLive ,
33
+ TimeSpan ? cleanupFrequency = null ,
34
+ long ? minCacheItemsBeforeCleanup = null ,
35
+ IDateTimeUtils ? dateTimeProvider = null )
36
+ {
37
+ _cache = new ConcurrentDictionary < TKey , ( TValue , DateTime ) > ( ) ;
38
+ TimeToLive = timeToLive ;
39
+ _minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup ;
40
+ _cleanupFrequency = cleanupFrequency ?? _defaultCleanupFrequency ;
41
+ _deleteExpiredCachedItemsDelegate = Cleanup ;
42
+ _dateTimeProvider = dateTimeProvider ?? new DateTimeUtils ( ) ;
43
+ }
44
+
45
+ public TimeSpan TimeToLive { get ; }
46
+
47
+ /// <summary>
48
+ /// Provide the number of items in the cache
49
+ /// </summary>
50
+ public int Count => _cache . Count ;
51
+
52
+ public void AddOrUpdate ( TKey key , TValue value )
53
+ {
54
+ Check . NotNull ( key ) ;
55
+ Check . NotNull ( value ) ;
56
+
57
+ var expirationTime = _dateTimeProvider . UtcNow . Add ( TimeToLive ) ;
58
+ _cache [ key ] = ( value , expirationTime ) ;
59
+
60
+ CleanupIfNeeded ( ) ;
61
+ }
62
+
63
+ public bool TryGetValue ( TKey key , out TValue value )
64
+ {
65
+ Check . NotNull ( key ) ;
66
+
67
+ CleanupIfNeeded ( ) ;
68
+
69
+ if ( _cache . TryGetValue ( key , out var valueAndExpiration ) )
70
+ {
71
+ if ( _dateTimeProvider . UtcNow <= valueAndExpiration . ExpirationTime )
72
+ {
73
+ value = valueAndExpiration . Value ;
74
+ _cache [ key ] = ( value , _dateTimeProvider . UtcNow . Add ( TimeToLive ) ) ;
75
+ return true ;
76
+ }
77
+
78
+ // Remove expired item
79
+ _cache . TryRemove ( key , out _ ) ;
80
+ }
81
+
82
+ value = default ! ;
83
+ return false ;
84
+ }
85
+
86
+ public bool Remove ( TKey key )
87
+ {
88
+ Check . NotNull ( key ) ;
89
+
90
+ var removed = _cache . TryRemove ( key , out _ ) ;
91
+ CleanupIfNeeded ( ) ;
92
+ return removed ;
93
+ }
94
+
95
+ /// <summary>
96
+ /// Check if cache needs to be cleaned up.
97
+ /// If it does, span the cleanup as a Task to prevent from blocking
98
+ /// </summary>
99
+ private void CleanupIfNeeded ( )
100
+ {
101
+ if ( _dateTimeProvider . UtcNow - _lastCleanupTime > _cleanupFrequency
102
+ && ( _minCacheItemsBeforeCleanup == null ||
103
+ _cache . Count >=
104
+ _minCacheItemsBeforeCleanup ) // Only cleanup if we have a minimum number of items in the cache.
105
+ )
106
+ {
107
+ // Set here, so we don't have re-entry due to large collection enumeration.
108
+ _lastCleanupTime = _dateTimeProvider . UtcNow ;
109
+
110
+ Task . Run ( _deleteExpiredCachedItemsDelegate ) ;
111
+ }
112
+ }
113
+
114
+ /// <summary>
115
+ /// Cleanup the Cache
116
+ /// </summary>
117
+ /// <returns></returns>
118
+ private Task Cleanup ( )
119
+ {
120
+ foreach ( var key in _cache . Keys )
121
+ {
122
+ if ( _dateTimeProvider . UtcNow > _cache [ key ] . ExpirationTime )
123
+ {
124
+ _cache . TryRemove ( key , out _ ) ;
125
+ }
126
+ }
127
+
128
+ return Task . CompletedTask ;
129
+ }
130
+ }
131
+ }
0 commit comments