• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JaCraig / DragonHoard / 22957659188

11 Mar 2026 02:29PM UTC coverage: 60.185% (-0.6%) from 60.754%
22957659188

push

github

JaCraig
chore(release): bump version to 2.3.4 and update changelog

Update project version to 2.3.4 and document recent changes.

- Bump version from 2.3.3 to 2.3.4 in all relevant .csproj files
- Add 2.3.4 release notes to CHANGELOG.md, including bug fixes and merged dependency update pull requests

116 of 238 branches covered (48.74%)

Branch coverage included in aggregate %.

274 of 410 relevant lines covered (66.83%)

192934.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.75
/DragonHoard.InMemory/InMemoryCache.cs
1
/*
2
Copyright 2021 James Craig
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
using DragonHoard.Core;
18
using DragonHoard.Core.BaseClasses;
19
using DragonHoard.Core.Interfaces;
20
using Microsoft.Extensions.Caching.Memory;
21
using Microsoft.Extensions.Options;
22
using System;
23
using System.Collections.Generic;
24
using System.Linq;
25
using System.Threading;
26
using System.Threading.Tasks;
27

28
namespace DragonHoard.InMemory
29
{
30
    /// <summary>
31
    /// In memory cache
32
    /// </summary>
33
    /// <seealso cref="ICache"/>
34
    public class InMemoryCache : CacheBaseClass
35
    {
36
        /// <summary>
37
        /// The lock object
38
        /// </summary>
39
        private readonly object _LockObject = new();
33✔
40

41
        /// <summary>
42
        /// The current size
43
        /// </summary>
44
        private long _CurrentSize;
45

46
        /// <summary>
47
        /// Gets or sets the last scan.
48
        /// </summary>
49
        /// <value>The last scan.</value>
50
        private DateTimeOffset _LastScan;
51

52
        /// <summary>
53
        /// Initializes a new instance of the <see cref="InMemoryCache"/> class.
54
        /// </summary>
55
        /// <param name="options">The options.</param>
56
        public InMemoryCache(IEnumerable<IOptions<InMemoryCacheOptions>> options)
33✔
57
        {
58
            Options = options.FirstOrDefault()?.Value ?? InMemoryCacheOptions.Default;
33!
59
            if (Options.ScanFrequency == default)
33✔
60
                Options.ScanFrequency = TimeSpan.FromMinutes(1);
1✔
61
            _LastScan = DateTimeOffset.UtcNow;
33✔
62
        }
33✔
63

64
        /// <summary>
65
        /// Gets the name.
66
        /// </summary>
67
        /// <value>The name.</value>
68
        public override string Name { get; } = "In Memory";
45✔
69

70
        /// <summary>
71
        /// Internal cache
72
        /// </summary>
73
        private Dictionary<int, CacheEntry>? InternalCache { get; set; } = [];
5,500,159✔
74

75
        /// <summary>
76
        /// Gets the options.
77
        /// </summary>
78
        /// <value>The options.</value>
79
        private InMemoryCacheOptions Options { get; }
2,300,090✔
80

81
        /// <summary>
82
        /// Clones this instance.
83
        /// </summary>
84
        /// <returns>A copy of this cache.</returns>
85
        public override ICache Clone() => new InMemoryCache([Options]);
21✔
86

87
        /// <summary>
88
        /// Clones this instance.
89
        /// </summary>
90
        /// <typeparam name="TOption">The type of the option.</typeparam>
91
        /// <param name="options">The options to use for the cache.</param>
92
        /// <returns>A copy of this cache.</returns>
93
        public override ICache Clone<TOption>(TOption options) => new InMemoryCache([options as IOptions<InMemoryCacheOptions> ?? Options]);
×
94

95
        /// <summary>
96
        /// Compacts the specified percentage.
97
        /// </summary>
98
        /// <param name="percentage">The percentage.</param>
99
        public override void Compact(double percentage)
100
        {
101
            if (InternalCache is null)
1!
102
                return;
×
103
            var CurrentCount = InternalCache.Count;
1✔
104
            var ItemsToRemove = (int)Math.Round(CurrentCount * percentage, MidpointRounding.AwayFromZero);
1✔
105
            var TargetCount = CurrentCount - ItemsToRemove;
1✔
106

107
            if (TargetCount == CurrentCount)
1!
108
                return;
×
109

110
            DateTimeOffset CurrentTime = DateTimeOffset.UtcNow;
1✔
111
            lock (_LockObject)
1✔
112
            {
113
                CacheEntry[] Items = [.. InternalCache.Values];
1✔
114
                var EntriesToRemove = new List<CacheEntry>();
1✔
115
                var LowPriorty = new List<CacheEntry>();
1✔
116
                var NormalPriorty = new List<CacheEntry>();
1✔
117
                var HighPriorty = new List<CacheEntry>();
1✔
118
                for (var I = 0; I < Items.Length; I++)
8✔
119
                {
120
                    CacheEntry Item = Items[I];
3✔
121
                    if (!CheckValid(Item, CurrentTime))
3!
122
                    {
123
                        Item.Invalid = true;
×
124
                        EntriesToRemove.Add(Item);
×
125
                        --CurrentCount;
×
126
                    }
127
                    else
128
                    {
129
                        switch (Item.Priority)
3!
130
                        {
131
                            case CachePriority.Low:
132
                            {
133
                                LowPriorty.Add(Item);
×
134
                                break;
×
135
                            }
136

137
                            case CachePriority.Normal:
138
                            {
139
                                NormalPriorty.Add(Item);
3✔
140
                                break;
3✔
141
                            }
142

143
                            case CachePriority.High:
144
                            {
145
                                HighPriorty.Add(Item);
×
146
                                break;
147
                            }
148
                        }
149
                    }
150
                }
151
                CurrentCount = ExpirePriorityBucket(CurrentCount, TargetCount, EntriesToRemove, LowPriorty);
1✔
152
                CurrentCount = ExpirePriorityBucket(CurrentCount, TargetCount, EntriesToRemove, NormalPriorty);
1✔
153
                CurrentCount = ExpirePriorityBucket(CurrentCount, TargetCount, EntriesToRemove, HighPriorty);
1✔
154
                foreach (CacheEntry Item in EntriesToRemove)
4✔
155
                {
156
                    _CurrentSize -= Item.Size ?? 0;
1✔
157
                    _ = InternalCache.Remove(Item.Key);
1✔
158
                    EvictionCallback(Item.Key, Item.Value, EvictionReason.Expired, this);
1✔
159
                }
160
            }
161
        }
1✔
162

163
        /// <summary>
164
        /// Performs application-defined tasks associated with freeing, releasing, or resetting
165
        /// unmanaged resources.
166
        /// </summary>
167
        public override void Dispose()
168
        {
169
            if (InternalCache is null)
11!
170
                return;
×
171
            foreach (CacheEntry Item in InternalCache.Values)
40✔
172
            {
173
                Item.Dispose();
9✔
174
            }
175
            InternalCache.Clear();
11✔
176
            InternalCache = null;
11✔
177
            GC.SuppressFinalize(this);
11✔
178
        }
11✔
179

180
        /// <summary>
181
        /// Sets the specified key/value pair in the cache.
182
        /// </summary>
183
        /// <typeparam name="TValue">The type of the value.</typeparam>
184
        /// <param name="key">The key.</param>
185
        /// <param name="value">The value.</param>
186
        /// <returns>The value sent in.</returns>
187
        public override TValue Set<TValue>(object key, TValue value)
188
        {
189
            if (InternalCache is null)
1,000,004!
190
                return value;
×
191
            DateTimeOffset UtcNow = DateTimeOffset.UtcNow;
1,000,004✔
192
            lock (_LockObject)
1,000,004✔
193
            {
194
                CacheEntry Entry = GetOrCreateEntry(key, value, UtcNow);
1,000,004✔
195
            }
1,000,004✔
196
            ScanForItemsToRemove(UtcNow);
1,000,004✔
197
            return value;
1,000,004✔
198
        }
199

200
        /// <summary>
201
        /// Sets the specified key/value pair in the cache.
202
        /// </summary>
203
        /// <typeparam name="TValue">The type of the value.</typeparam>
204
        /// <param name="key">The key.</param>
205
        /// <param name="value">The value.</param>
206
        /// <param name="absoluteExpiration">The absolute expiration.</param>
207
        /// <returns>The value sent in.</returns>
208
        public override TValue Set<TValue>(object key, TValue value, DateTimeOffset absoluteExpiration)
209
        {
210
            if (InternalCache is null)
×
211
                return value;
×
212
            DateTimeOffset UtcNow = DateTimeOffset.UtcNow;
×
213
            lock (_LockObject)
×
214
            {
215
                CacheEntry Entry = GetOrCreateEntry(key, value, UtcNow);
×
216
                Entry.AbsoluteExpiration = absoluteExpiration;
×
217
            }
×
218
            ScanForItemsToRemove(UtcNow);
×
219
            return value;
×
220
        }
221

222
        /// <summary>
223
        /// Sets the specified key/value pair in the cache.
224
        /// </summary>
225
        /// <typeparam name="TValue">The type of the value.</typeparam>
226
        /// <param name="key">The key.</param>
227
        /// <param name="value">The value.</param>
228
        /// <param name="expirationRelativeToNow">The expiration relative to now.</param>
229
        /// <param name="sliding">if set to <c>true</c> [sliding] expiration.</param>
230
        /// <returns>The value sent in.</returns>
231
        public override TValue Set<TValue>(object key, TValue value, TimeSpan expirationRelativeToNow, bool sliding = false)
232
        {
233
            if (InternalCache is null)
×
234
                return value;
×
235
            DateTimeOffset UtcNow = DateTimeOffset.UtcNow;
×
236
            lock (_LockObject)
×
237
            {
238
                CacheEntry Entry = GetOrCreateEntry(key, value, UtcNow);
×
239
                if (sliding)
×
240
                    Entry.SlidingExpiration = expirationRelativeToNow;
×
241
                else
242
                    Entry.AbsoluteExpiration = UtcNow + expirationRelativeToNow;
×
243
            }
×
244
            ScanForItemsToRemove(UtcNow);
×
245
            return value;
×
246
        }
247

248
        /// <summary>
249
        /// Tries to get the value based on the key.
250
        /// </summary>
251
        /// <typeparam name="TValue">The type of the value.</typeparam>
252
        /// <param name="key">The key.</param>
253
        /// <param name="value">The value.</param>
254
        /// <returns>True if it is successful, false otherwise</returns>
255
        public override bool TryGetValue<TValue>(object key, out TValue value)
256
        {
257
            if (InternalCache is null)
1,100,009!
258
            {
259
                value = default!;
×
260
                return false;
×
261
            }
262
            bool ReturnValue;
263
            DateTimeOffset CurrentTime = DateTimeOffset.UtcNow;
1,100,009✔
264
            lock (_LockObject)
1,100,009✔
265
            {
266
                ReturnValue = InternalCache.TryGetValue(key.GetHashCode(), out CacheEntry? TempValue);
1,100,009✔
267

268
                if (ReturnValue)
1,100,009✔
269
                {
270
                    if (CheckValid(TempValue, CurrentTime) && TempValue is not null)
1,100,007!
271
                    {
272
                        value = (TValue)TempValue.Value!;
1,100,007✔
273
                        TempValue.LastAccessed = CurrentTime;
1,100,007✔
274
                    }
275
                    else
276
                    {
277
                        ReturnValue = false;
×
278
                        value = default!;
×
279
                    }
280
                }
281
                else
282
                {
283
                    value = default!;
2✔
284
                }
285
            }
2✔
286
            ScanForItemsToRemove(CurrentTime);
1,100,009✔
287
            return ReturnValue;
1,100,009✔
288
        }
289

290
        /// <summary>
291
        /// Removes the items by the key.
292
        /// </summary>
293
        /// <param name="key">The key.</param>
294
        protected override void RemoveByKey(object key)
295
        {
296
            if (InternalCache is null)
2!
297
                return;
×
298
            DateTimeOffset CurrentTime = DateTimeOffset.UtcNow;
2✔
299
            lock (_LockObject)
2✔
300
            {
301
                var HashKey = key.GetHashCode();
2✔
302
                if (InternalCache.TryGetValue(HashKey, out CacheEntry? Current))
2✔
303
                {
304
                    _ = InternalCache.Remove(HashKey);
2✔
305
                    _CurrentSize -= Current.Size ?? 0;
2✔
306
                }
307
            }
2✔
308
            ScanForItemsToRemove(CurrentTime);
2✔
309
        }
2✔
310

311
        /// <summary>
312
        /// Sets the value with the options sent in.
313
        /// </summary>
314
        /// <typeparam name="TValue">The type of the value.</typeparam>
315
        /// <param name="key">The key.</param>
316
        /// <param name="value">The value.</param>
317
        /// <param name="cacheEntryOptions">The cache entry options.</param>
318
        /// <returns>The value sent in.</returns>
319
        protected override TValue SetWithOptions<TValue>(object key, TValue value, CacheEntryOptions cacheEntryOptions)
320
        {
321
            if (InternalCache is null)
100,010!
322
                return value;
×
323
            var ExceedsSize = Options.MaxCacheSize.HasValue && cacheEntryOptions.Size.HasValue && cacheEntryOptions.Size + _CurrentSize > Options.MaxCacheSize;
100,010!
324
            DateTimeOffset UtcNow = DateTimeOffset.UtcNow;
100,010✔
325
            lock (_LockObject)
100,010✔
326
            {
327
                CacheEntry Entry = GetOrCreateEntry(key, value, UtcNow);
100,010✔
328
                if (cacheEntryOptions.AbsoluteExpiration.HasValue)
100,010!
329
                    Entry.AbsoluteExpiration = cacheEntryOptions.AbsoluteExpiration;
×
330
                if (cacheEntryOptions.AbsoluteExpirationRelativeToNow.HasValue)
100,010!
331
                    Entry.AbsoluteExpiration = UtcNow + cacheEntryOptions.AbsoluteExpirationRelativeToNow;
×
332
                if (cacheEntryOptions.SlidingExpiration.HasValue)
100,010!
333
                    Entry.SlidingExpiration = cacheEntryOptions.SlidingExpiration;
×
334
                if (cacheEntryOptions.Size.HasValue)
100,010!
335
                {
336
                    Entry.Size = cacheEntryOptions.Size;
×
337
                    _CurrentSize += Entry.Size.Value;
×
338
                }
339
                Entry.Priority = cacheEntryOptions.Priority;
100,010✔
340
            }
100,010✔
341
            ScanForItemsToRemove(UtcNow);
100,010✔
342
            if (ExceedsSize)
100,010!
343
            {
344
                Compact(Options.CompactionPercentage ?? 0.1);
×
345
            }
346
            return value;
100,010✔
347
        }
348

349
        /// <summary>
350
        /// Checks to see if the entry is still valid.
351
        /// </summary>
352
        /// <param name="tempValue">The temporary value.</param>
353
        /// <param name="currentTime">The current time.</param>
354
        /// <returns>True if it is, false otherwise.</returns>
355
        private static bool CheckValid(CacheEntry? tempValue, DateTimeOffset currentTime)
356
        {
357
            if (tempValue is null || tempValue.Invalid)
1,100,010!
358
            {
359
                return false;
×
360
            }
361

362
            if (!tempValue.AbsoluteExpiration.HasValue && !tempValue.SlidingExpiration.HasValue)
1,100,010!
363
            {
364
                return true;
1,100,010✔
365
            }
366

367
            if (tempValue.AbsoluteExpiration.HasValue && tempValue.AbsoluteExpiration <= currentTime)
×
368
            {
369
                tempValue.Invalid = true;
×
370
                return false;
×
371
            }
372
            if (tempValue.SlidingExpiration.HasValue && tempValue.LastAccessed + tempValue.SlidingExpiration <= currentTime)
×
373
            {
374
                tempValue.Invalid = true;
×
375
                return false;
×
376
            }
377
            return true;
×
378
        }
379

380
        /// <summary>
381
        /// Expires the priority bucket.
382
        /// </summary>
383
        /// <param name="currentCount">The current count.</param>
384
        /// <param name="targetCount">The target count.</param>
385
        /// <param name="entriesToRemove">The entries to remove.</param>
386
        /// <param name="bucket">The bucket.</param>
387
        /// <returns>The number of items left.</returns>
388
        private static int ExpirePriorityBucket(int currentCount, int targetCount, List<CacheEntry> entriesToRemove, List<CacheEntry> bucket)
389
        {
390
            if (targetCount >= currentCount)
3✔
391
                return currentCount;
1✔
392
            foreach (CacheEntry? Item in bucket.Where(x => x.AbsoluteExpiration.HasValue && !x.Invalid).OrderBy(x => x.AbsoluteExpiration))
7!
393
            {
394
                Item.Invalid = true;
×
395
                entriesToRemove.Add(Item);
×
396
                --currentCount;
×
397
                if (targetCount >= currentCount)
×
398
                    return currentCount;
×
399
            }
400
            foreach (CacheEntry? Item in bucket.Where(x => x.SlidingExpiration.HasValue && !x.Invalid).OrderBy(x => x.SlidingExpiration))
7!
401
            {
402
                Item.Invalid = true;
×
403
                entriesToRemove.Add(Item);
×
404
                --currentCount;
×
405
                if (targetCount >= currentCount)
×
406
                    return currentCount;
×
407
            }
408
            foreach (CacheEntry? Item in bucket.Where(x => !x.Invalid).OrderBy(x => x.LastAccessed))
11✔
409
            {
410
                Item.Invalid = true;
1✔
411
                entriesToRemove.Add(Item);
1✔
412
                --currentCount;
1✔
413
                if (targetCount >= currentCount)
1✔
414
                    return currentCount;
1✔
415
            }
416
            return currentCount;
1✔
417
        }
1✔
418

419
        /// <summary>
420
        /// Gets or creates the entry.
421
        /// </summary>
422
        /// <typeparam name="TValue">The type of the value.</typeparam>
423
        /// <param name="key">The key.</param>
424
        /// <param name="value">The value.</param>
425
        /// <param name="currentTime">The current time.</param>
426
        /// <returns>The entry</returns>
427
        private CacheEntry GetOrCreateEntry<TValue>(object key, TValue value, DateTimeOffset currentTime)
428
        {
429
            if (InternalCache is null)
1,100,014!
430
                return CacheEntry.Empty;
×
431
            var HashKey = key.GetHashCode();
1,100,014✔
432
            if (InternalCache.TryGetValue(HashKey, out CacheEntry? Current))
1,100,014✔
433
            {
434
                if (Current.Value is IDisposable Disposable)
1,100,002!
435
                    Disposable.Dispose();
×
436
                Current.LastAccessed = currentTime;
1,100,002✔
437
                Current.AbsoluteExpiration = null;
1,100,002✔
438
                Current.SlidingExpiration = null;
1,100,002✔
439
                Current.Invalid = false;
1,100,002✔
440
                Current.Size = 0;
1,100,002✔
441
            }
442
            else
443
            {
444
                Current = new CacheEntry { LastAccessed = currentTime };
12✔
445
                InternalCache[HashKey] = Current;
12✔
446
            }
447
            Current.Key = HashKey;
1,100,014✔
448
            Current.Value = value;
1,100,014✔
449
            return Current;
1,100,014✔
450
        }
451

452
        /// <summary>
453
        /// Scans for items to remove.
454
        /// </summary>
455
        private void ScanForItemsToRemove(DateTimeOffset currentTime)
456
        {
457
            if (_LastScan + Options.ScanFrequency > currentTime)
2,200,025!
458
            {
459
                return;
2,200,025✔
460
            }
461

462
            _LastScan = currentTime;
×
463
            _ = Task.Factory.StartNew(state => ScanForItemsToRemove((InMemoryCache?)state), this, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
×
464
        }
×
465

466
        /// <summary>
467
        /// Scans for items to remove.
468
        /// </summary>
469
        /// <param name="cache">The state.</param>
470
        private void ScanForItemsToRemove(InMemoryCache? cache)
471
        {
472
            if (cache?.InternalCache is null)
×
473
                return;
×
474
            DateTimeOffset CurrentTime = DateTimeOffset.UtcNow;
×
475
            lock (_LockObject)
×
476
            {
477
                CacheEntry[] Items = [.. cache.InternalCache.Values.Where(x => !CheckValid(x, CurrentTime))];
×
478
                for (var I = 0; I < Items.Length; I++)
×
479
                {
480
                    CacheEntry Item = Items[I];
×
481
                    if (Item.Value is IDisposable Disposable)
×
482
                    {
483
                        Disposable.Dispose();
×
484
                    }
485
                    cache._CurrentSize -= Item.Size ?? 0;
×
486
                    _ = cache.InternalCache.Remove(Item.Key);
×
487
                    cache.EvictionCallback(Item.Key, Item.Value, EvictionReason.Expired, cache);
×
488
                }
489
            }
×
490
        }
×
491
    }
492
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc