From 443379e1e666961e2fb39482dd10776caa07fb54 Mon Sep 17 00:00:00 2001 From: Leefrost Date: Sat, 15 Apr 2023 22:50:37 +0300 Subject: [PATCH 1/6] Extend MemoryCacheEntry with options --- src/HttpClient.Cache/HttpClient.Cache.csproj | 2 +- src/HttpClient.Cache/InMemory/MemoryCache.cs | 30 +++++----- .../{CacheEntry.cs => MemoryCacheEntry.cs} | 18 +++--- .../InMemory/MemoryCacheEntryOptions.cs | 59 +++++++++++++++++++ .../InMemory/MemoryCacheExtensions.cs | 2 +- 5 files changed, 85 insertions(+), 26 deletions(-) rename src/HttpClient.Cache/InMemory/{CacheEntry.cs => MemoryCacheEntry.cs} (92%) create mode 100644 src/HttpClient.Cache/InMemory/MemoryCacheEntryOptions.cs diff --git a/src/HttpClient.Cache/HttpClient.Cache.csproj b/src/HttpClient.Cache/HttpClient.Cache.csproj index 240a9a0..836ad13 100644 --- a/src/HttpClient.Cache/HttpClient.Cache.csproj +++ b/src/HttpClient.Cache/HttpClient.Cache.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/HttpClient.Cache/InMemory/MemoryCache.cs b/src/HttpClient.Cache/InMemory/MemoryCache.cs index aded4fc..823bcc2 100644 --- a/src/HttpClient.Cache/InMemory/MemoryCache.cs +++ b/src/HttpClient.Cache/InMemory/MemoryCache.cs @@ -6,16 +6,16 @@ namespace HttpClient.Cache.InMemory; public sealed class MemoryCache : IMemoryCache { private readonly ISystemClock _clock; - private readonly ConcurrentDictionary _cacheEntries; - private readonly Action _entryExpirationNotification; + private readonly ConcurrentDictionary _cacheEntries; + private readonly Action _entryExpirationNotification; private readonly TimeSpan _expirationScanFrequency; - private readonly Action _setEntry; + private readonly Action _setEntry; private bool _isDisposed; private DateTimeOffset _lastExpirationScan; - private ICollection> CacheEntries => _cacheEntries; + private ICollection> CacheEntries => _cacheEntries; public MemoryCache() : this(new MemoryCacheOptions()) @@ -29,7 +29,7 @@ public MemoryCache(MemoryCacheOptions options) throw new ArgumentNullException(nameof(options)); } - _cacheEntries = new ConcurrentDictionary(); + _cacheEntries = new ConcurrentDictionary(); _setEntry = SetEntry; _entryExpirationNotification = EntryExpired; @@ -52,7 +52,7 @@ public ICacheEntry CreateEntry(object key) throw new ObjectDisposedException(typeof(MemoryCache).FullName); } - return new CacheEntry(key, _setEntry, _entryExpirationNotification); + return new MemoryCacheEntry(key, _setEntry, _entryExpirationNotification); } public bool TryGetValue(object key, out object? value) @@ -71,7 +71,7 @@ public bool TryGetValue(object key, out object? value) var currentTime = _clock.UtcNow; bool isEntryFound = false; - if (_cacheEntries.TryGetValue(key, out CacheEntry? entry)) + if (_cacheEntries.TryGetValue(key, out MemoryCacheEntry? entry)) { if (entry.IsExpired(currentTime) && entry.EvictionReason != EvictionReason.Replaced) { @@ -101,7 +101,7 @@ public void Remove(object key) throw new ArgumentNullException(nameof(key)); } - if (_cacheEntries.TryRemove(key, out CacheEntry? cacheEntry)) + if (_cacheEntries.TryRemove(key, out MemoryCacheEntry? cacheEntry)) { cacheEntry.ExpireEntryByReason(EvictionReason.Removed); cacheEntry.InvokeEvictionCallbacks(); @@ -120,7 +120,7 @@ public void Clear() var keys = _cacheEntries.Keys.ToList(); foreach (object key in keys) { - if (_cacheEntries.TryRemove(key, out CacheEntry? cacheEntry)) + if (_cacheEntries.TryRemove(key, out MemoryCacheEntry? cacheEntry)) { cacheEntry.ExpireEntryByReason(EvictionReason.Removed); cacheEntry.InvokeEvictionCallbacks(); @@ -135,7 +135,7 @@ public void Dispose() Dispose(true); } - private void SetEntry(CacheEntry entry) + private void SetEntry(MemoryCacheEntry entry) { if (_isDisposed) { @@ -162,7 +162,7 @@ private void SetEntry(CacheEntry entry) entry.LastAccessed = currentTime; - if (_cacheEntries.TryGetValue(entry.Key, out CacheEntry? cacheEntry)) + if (_cacheEntries.TryGetValue(entry.Key, out MemoryCacheEntry? cacheEntry)) { cacheEntry.ExpireEntryByReason(EvictionReason.Replaced); } @@ -207,9 +207,9 @@ private void SetEntry(CacheEntry entry) StartScanForExpiredItems(); } - private void RemoveEntry(CacheEntry entry) + private void RemoveEntry(MemoryCacheEntry entry) { - if (!CacheEntries.Remove(new KeyValuePair(entry.Key, entry))) + if (!CacheEntries.Remove(new KeyValuePair(entry.Key, entry))) { return; } @@ -217,7 +217,7 @@ private void RemoveEntry(CacheEntry entry) entry.InvokeEvictionCallbacks(); } - private void EntryExpired(CacheEntry entry) + private void EntryExpired(MemoryCacheEntry entry) { RemoveEntry(entry); StartScanForExpiredItems(); @@ -240,7 +240,7 @@ private void StartScanForExpiredItems() private static void ScanForExpiredItems(MemoryCache cache) { var currentTime = cache._clock.UtcNow; - foreach (CacheEntry entry in cache._cacheEntries.Values) + foreach (MemoryCacheEntry entry in cache._cacheEntries.Values) { if (entry.IsExpired(currentTime)) { diff --git a/src/HttpClient.Cache/InMemory/CacheEntry.cs b/src/HttpClient.Cache/InMemory/MemoryCacheEntry.cs similarity index 92% rename from src/HttpClient.Cache/InMemory/CacheEntry.cs rename to src/HttpClient.Cache/InMemory/MemoryCacheEntry.cs index 36b1cb3..3b91de9 100644 --- a/src/HttpClient.Cache/InMemory/CacheEntry.cs +++ b/src/HttpClient.Cache/InMemory/MemoryCacheEntry.cs @@ -2,13 +2,13 @@ namespace HttpClient.Cache.InMemory; -internal class CacheEntry : ICacheEntry +internal class MemoryCacheEntry : ICacheEntry { private readonly object _lock = new(); private static readonly Action ExpirationCallback = ExpirationTokensExpired; - private readonly Action _notifyCacheEntryDisposed; - private readonly Action _notifyCacheOfExpiration; + private readonly Action _notifyCacheEntryDisposed; + private readonly Action _notifyCacheOfExpiration; private DateTimeOffset? _absoluteExpiration; private TimeSpan? _absoluteExpirationRelativeToNow; private TimeSpan? _slidingExpiration; @@ -20,10 +20,10 @@ internal class CacheEntry : ICacheEntry private bool _isDisposed; private bool _isExpired; - internal CacheEntry( + internal MemoryCacheEntry( object key, - Action notifyCacheEntryDisposed, - Action notifyCacheOfExpiration) + Action notifyCacheEntryDisposed, + Action notifyCacheOfExpiration) { Key = key ?? throw new ArgumentNullException(nameof(key)); @@ -158,7 +158,7 @@ private static void ExpirationTokensExpired(object entry) { Task.Factory.StartNew(state => { - CacheEntry? cacheEntry = state as CacheEntry; + MemoryCacheEntry? cacheEntry = state as MemoryCacheEntry; cacheEntry?.ExpireEntryByReason(EvictionReason.TokenExpired); cacheEntry?._notifyCacheOfExpiration(cacheEntry); }, entry, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); @@ -222,11 +222,11 @@ internal void InvokeEvictionCallbacks() return; } - Task.Factory.StartNew(state => InvokeCallbacks((CacheEntry)state!), this, CancellationToken.None, + Task.Factory.StartNew(state => InvokeCallbacks((MemoryCacheEntry)state!), this, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } - private static void InvokeCallbacks(CacheEntry entry) + private static void InvokeCallbacks(MemoryCacheEntry entry) { var evictionCallbacks = Interlocked.Exchange(ref entry._postEvictionCallbacks, null); diff --git a/src/HttpClient.Cache/InMemory/MemoryCacheEntryOptions.cs b/src/HttpClient.Cache/InMemory/MemoryCacheEntryOptions.cs new file mode 100644 index 0000000..f203407 --- /dev/null +++ b/src/HttpClient.Cache/InMemory/MemoryCacheEntryOptions.cs @@ -0,0 +1,59 @@ +namespace HttpClient.Cache.InMemory; + +public class MemoryCacheEntryOptions +{ + private TimeSpan? _absoluteExpirationRelativeToNow; + + private TimeSpan? _slidingExpiration; + + private DateTimeOffset? _absoluteExpiration; + + public DateTimeOffset? AbsoluteExpiration + { + get { return _absoluteExpiration; } + set + { + if (value.HasValue && value.Value < DateTimeOffset.Now) + throw new ArgumentOutOfRangeException(nameof(AbsoluteExpiration), value, + "The absolute expiration can not be in the past"); + + _absoluteExpiration = value; + } + } + + public TimeSpan? AbsoluteExpirationRelativeToNow + { + get + { + return _absoluteExpirationRelativeToNow; + } + set + { + if ((value.HasValue ? (value.GetValueOrDefault() <= TimeSpan.Zero ? 1 : 0) : 0) != 0) + throw new ArgumentOutOfRangeException(nameof(AbsoluteExpirationRelativeToNow), value, + "Relative expiration must be a positive"); + + _absoluteExpirationRelativeToNow = value; + } + } + + public TimeSpan? SlidingExpiration + { + get { return _slidingExpiration; } + set + { + if ((value.HasValue ? (value.GetValueOrDefault() <= TimeSpan.Zero ? 1 : 0) : 0) != 0) + throw new ArgumentOutOfRangeException(nameof(SlidingExpiration), value, + "Sliding expiration must be positive"); + + _slidingExpiration = value; + } + } + + public CacheEntryPriority Priority { get; set; } = CacheEntryPriority.Normal; + + public IList ExpirationTokens { get; } = new List(); + + public IList PostEvictionCallbacks { get; } = + new List(); +} \ No newline at end of file diff --git a/src/HttpClient.Cache/InMemory/MemoryCacheExtensions.cs b/src/HttpClient.Cache/InMemory/MemoryCacheExtensions.cs index 145c278..49a8f32 100644 --- a/src/HttpClient.Cache/InMemory/MemoryCacheExtensions.cs +++ b/src/HttpClient.Cache/InMemory/MemoryCacheExtensions.cs @@ -2,7 +2,7 @@ namespace HttpClient.Cache.InMemory; -internal static class MemoryCacheExtensions +public static class MemoryCacheExtensions { public static Task TryGetAsync(this IMemoryCache cache, string key, out CacheData? cacheData) { From 20343392cdc807b4563ca35eb4a0f4c25ba05faf Mon Sep 17 00:00:00 2001 From: Leefrost Date: Sun, 16 Apr 2023 13:34:28 +0300 Subject: [PATCH 2/6] Added docs and updated default config for memory cache --- .../InMemory/Clock/DefaultSystemClock.cs | 12 ++++++++++ .../InMemory/Clock/SystemClock.cs | 6 ----- src/HttpClient.Cache/InMemory/MemoryCache.cs | 2 +- .../InMemory/MemoryCacheEntryOptions.cs | 24 +++++++++++++++++++ .../InMemory/MemoryCacheOptions.cs | 11 ++++++++- ...ockTests.cs => DefaultSystemClockTests.cs} | 4 ++-- .../DefaultMemoryCacheOptionsTests.cs | 19 +++++++++++++++ 7 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 src/HttpClient.Cache/InMemory/Clock/DefaultSystemClock.cs delete mode 100644 src/HttpClient.Cache/InMemory/Clock/SystemClock.cs rename tests/HttpClient.Cache.Tests/InMemory/Clock/{SystemClockTests.cs => DefaultSystemClockTests.cs} (76%) create mode 100644 tests/HttpClient.Cache.Tests/InMemory/DefaultMemoryCacheOptionsTests.cs diff --git a/src/HttpClient.Cache/InMemory/Clock/DefaultSystemClock.cs b/src/HttpClient.Cache/InMemory/Clock/DefaultSystemClock.cs new file mode 100644 index 0000000..3395471 --- /dev/null +++ b/src/HttpClient.Cache/InMemory/Clock/DefaultSystemClock.cs @@ -0,0 +1,12 @@ +namespace HttpClient.Cache.InMemory.Clock; + +/// +/// Presents current UTC time +/// +internal class DefaultSystemClock: ISystemClock +{ + /// + /// Current UTC now offset. + /// + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; +} \ No newline at end of file diff --git a/src/HttpClient.Cache/InMemory/Clock/SystemClock.cs b/src/HttpClient.Cache/InMemory/Clock/SystemClock.cs deleted file mode 100644 index 3e768cc..0000000 --- a/src/HttpClient.Cache/InMemory/Clock/SystemClock.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace HttpClient.Cache.InMemory.Clock; - -internal class SystemClock: ISystemClock -{ - public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; -} \ No newline at end of file diff --git a/src/HttpClient.Cache/InMemory/MemoryCache.cs b/src/HttpClient.Cache/InMemory/MemoryCache.cs index 823bcc2..7647afb 100644 --- a/src/HttpClient.Cache/InMemory/MemoryCache.cs +++ b/src/HttpClient.Cache/InMemory/MemoryCache.cs @@ -33,7 +33,7 @@ public MemoryCache(MemoryCacheOptions options) _setEntry = SetEntry; _entryExpirationNotification = EntryExpired; - _clock = options.Clock ?? new SystemClock(); + _clock = options.Clock ?? new DefaultSystemClock(); _expirationScanFrequency = options.ExpirationScanFrequency; _lastExpirationScan = _clock.UtcNow; } diff --git a/src/HttpClient.Cache/InMemory/MemoryCacheEntryOptions.cs b/src/HttpClient.Cache/InMemory/MemoryCacheEntryOptions.cs index f203407..b095ba3 100644 --- a/src/HttpClient.Cache/InMemory/MemoryCacheEntryOptions.cs +++ b/src/HttpClient.Cache/InMemory/MemoryCacheEntryOptions.cs @@ -1,5 +1,8 @@ namespace HttpClient.Cache.InMemory; +/// +/// Provides cache entry configuration +/// public class MemoryCacheEntryOptions { private TimeSpan? _absoluteExpirationRelativeToNow; @@ -8,6 +11,10 @@ public class MemoryCacheEntryOptions private DateTimeOffset? _absoluteExpiration; + /// + /// The absolute expiration for cache entry. The point in time where cache entry will no longer be available + /// + /// Time point must not be in the past public DateTimeOffset? AbsoluteExpiration { get { return _absoluteExpiration; } @@ -21,6 +28,10 @@ public DateTimeOffset? AbsoluteExpiration } } + /// + /// The absolute expiration due to now. The time period while entity will be available from now. + /// + /// Time point should be positive and bigger to value public TimeSpan? AbsoluteExpirationRelativeToNow { get @@ -37,6 +48,10 @@ public TimeSpan? AbsoluteExpirationRelativeToNow } } + /// + /// Cache entry sliding expiration. The un-active time for cache entry. Do not extends the if it set. + /// + /// Time point should be positive and bigger to value public TimeSpan? SlidingExpiration { get { return _slidingExpiration; } @@ -50,10 +65,19 @@ public TimeSpan? SlidingExpiration } } + /// + /// The cache entry priority. Default value is + /// public CacheEntryPriority Priority { get; set; } = CacheEntryPriority.Normal; + /// + /// The collection of expiration tokens + /// public IList ExpirationTokens { get; } = new List(); + /// + /// The collection of eviction callbacks + /// public IList PostEvictionCallbacks { get; } = new List(); } \ No newline at end of file diff --git a/src/HttpClient.Cache/InMemory/MemoryCacheOptions.cs b/src/HttpClient.Cache/InMemory/MemoryCacheOptions.cs index 1af2a38..8227d4a 100644 --- a/src/HttpClient.Cache/InMemory/MemoryCacheOptions.cs +++ b/src/HttpClient.Cache/InMemory/MemoryCacheOptions.cs @@ -2,9 +2,18 @@ namespace HttpClient.Cache.InMemory; +/// +/// Provides memory cache configuration +/// public class MemoryCacheOptions { + /// + /// Time frequency to check the cache entries expiration. Default value is 1 minute. + /// public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromMinutes(1.0); - public ISystemClock Clock { get; set; } = new SystemClock(); + /// + /// Internal system clock. Default value is clock + /// + public ISystemClock Clock { get; set; } = new DefaultSystemClock(); } \ No newline at end of file diff --git a/tests/HttpClient.Cache.Tests/InMemory/Clock/SystemClockTests.cs b/tests/HttpClient.Cache.Tests/InMemory/Clock/DefaultSystemClockTests.cs similarity index 76% rename from tests/HttpClient.Cache.Tests/InMemory/Clock/SystemClockTests.cs rename to tests/HttpClient.Cache.Tests/InMemory/Clock/DefaultSystemClockTests.cs index 4377044..ecbabb2 100644 --- a/tests/HttpClient.Cache.Tests/InMemory/Clock/SystemClockTests.cs +++ b/tests/HttpClient.Cache.Tests/InMemory/Clock/DefaultSystemClockTests.cs @@ -3,12 +3,12 @@ namespace HttpClient.Cache.Tests.InMemory.Clock; -public class SystemClockTests +public class DefaultSystemClockTests { [Fact] public void GetTime_CheckCurrentTime_ReturnUtcNow() { - var systemClock = new SystemClock(); + var systemClock = new DefaultSystemClock(); systemClock.UtcNow.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(1)); } diff --git a/tests/HttpClient.Cache.Tests/InMemory/DefaultMemoryCacheOptionsTests.cs b/tests/HttpClient.Cache.Tests/InMemory/DefaultMemoryCacheOptionsTests.cs new file mode 100644 index 0000000..6b059db --- /dev/null +++ b/tests/HttpClient.Cache.Tests/InMemory/DefaultMemoryCacheOptionsTests.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using HttpClient.Cache.InMemory; +using HttpClient.Cache.InMemory.Clock; + +namespace HttpClient.Cache.Tests.InMemory; + +public class DefaultMemoryCacheOptionsTests +{ + [Fact] + public void Default_CheckDefaultOptionsForMemoryCache_ReturnExpectedConfiguration() + { + var expectedScanFrequency = TimeSpan.FromMinutes(1.0); + + var options = new MemoryCacheOptions(); + + options.ExpirationScanFrequency.Should().Be(expectedScanFrequency); + options.Clock.Should().NotBeNull().And.BeOfType(); + } +} \ No newline at end of file From 5c080c1a86542344ac1fd79afec3fddac942d6dd Mon Sep 17 00:00:00 2001 From: Leefrost Date: Mon, 17 Apr 2023 21:17:45 +0300 Subject: [PATCH 3/6] Update API for memory and cache entries --- src/HttpClient.Cache/CacheEntryExtensions.cs | 46 ++++++++++++++++++- .../InMemory/MemoryCacheEntryExtensions.cs | 24 ++++++++++ .../PostEvictionCallbackRegistration.cs | 8 +--- 3 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 src/HttpClient.Cache/InMemory/MemoryCacheEntryExtensions.cs diff --git a/src/HttpClient.Cache/CacheEntryExtensions.cs b/src/HttpClient.Cache/CacheEntryExtensions.cs index 808e7ee..279f3fa 100644 --- a/src/HttpClient.Cache/CacheEntryExtensions.cs +++ b/src/HttpClient.Cache/CacheEntryExtensions.cs @@ -1,8 +1,38 @@ namespace HttpClient.Cache; -internal static class CacheEntryExtensions +public static class CacheEntryExtensions { - internal static ICacheEntry AddExpirationToken(this ICacheEntry entry, IChangeToken token) + public static ICacheEntry SetValue(this ICacheEntry entry, object value) + { + entry.Value = value; + return entry; + } + + public static ICacheEntry SetPriority(this ICacheEntry entry, CacheEntryPriority priority) + { + entry.Priority = priority; + return entry; + } + + public static ICacheEntry SetAbsoluteExpiration(this ICacheEntry entry, DateTimeOffset expiredAt) + { + entry.AbsoluteExpiration = expiredAt; + return entry; + } + + public static ICacheEntry SetAbsoluteExpiration(this ICacheEntry entry, TimeSpan expiredAt) + { + entry.AbsoluteExpirationRelativeToNow = expiredAt; + return entry; + } + + public static ICacheEntry SetSlidingExpiration(this ICacheEntry entry, TimeSpan slidingExpiration) + { + entry.SlidingExpiration = slidingExpiration; + return entry; + } + + public static ICacheEntry AddExpirationToken(this ICacheEntry entry, IChangeToken token) { if (token == null) { @@ -12,4 +42,16 @@ internal static ICacheEntry AddExpirationToken(this ICacheEntry entry, IChangeTo entry.ExpirationTokens.Add(token); return entry; } + + public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, PostEvictionDelegate callback, + object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + entry.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration(callback, state)); + return entry; + } } \ No newline at end of file diff --git a/src/HttpClient.Cache/InMemory/MemoryCacheEntryExtensions.cs b/src/HttpClient.Cache/InMemory/MemoryCacheEntryExtensions.cs new file mode 100644 index 0000000..e7840f6 --- /dev/null +++ b/src/HttpClient.Cache/InMemory/MemoryCacheEntryExtensions.cs @@ -0,0 +1,24 @@ +namespace HttpClient.Cache.InMemory; + +public static class MemoryCacheEntryExtensions +{ + public static ICacheEntry SetOptions(this ICacheEntry entry, MemoryCacheEntryOptions options) + { + entry.AbsoluteExpiration = options.AbsoluteExpiration; + entry.AbsoluteExpirationRelativeToNow = options.AbsoluteExpirationRelativeToNow; + entry.SlidingExpiration = options.SlidingExpiration; + entry.Priority = options.Priority; + + foreach (var expirationToken in options.ExpirationTokens) + { + entry.ExpirationTokens.Add(expirationToken); + } + + foreach (var evictionCallback in options.PostEvictionCallbacks) + { + entry.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration(evictionCallback.EvictionCallback, evictionCallback.State)); + } + + return entry; + } +} \ No newline at end of file diff --git a/src/HttpClient.Cache/PostEvictionCallbackRegistration.cs b/src/HttpClient.Cache/PostEvictionCallbackRegistration.cs index 7403c68..5015fbb 100644 --- a/src/HttpClient.Cache/PostEvictionCallbackRegistration.cs +++ b/src/HttpClient.Cache/PostEvictionCallbackRegistration.cs @@ -1,11 +1,5 @@ namespace HttpClient.Cache; -public class PostEvictionCallbackRegistration -{ - //TODO: Replace with constructor - callback is not exists without state - public PostEvictionDelegate? EvictionCallback { get; set; } - - public object? State { get; set; } -} +public record PostEvictionCallbackRegistration(PostEvictionDelegate? EvictionCallback, object? State); public delegate void PostEvictionDelegate(object key, object? value, string reason, object? state); \ No newline at end of file From 425a288dd23459d66fec5e9d58b5d750717e5a33 Mon Sep 17 00:00:00 2001 From: Leefrost Date: Tue, 18 Apr 2023 22:19:07 +0300 Subject: [PATCH 4/6] Update SetOptions tests --- .../InMemory/MemoryCacheEntry.cs | 11 +++- .../MemoryCacheEntryExtensionsTests.cs | 65 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 tests/HttpClient.Cache.Tests/InMemory/MemoryCacheEntryExtensionsTests.cs diff --git a/src/HttpClient.Cache/InMemory/MemoryCacheEntry.cs b/src/HttpClient.Cache/InMemory/MemoryCacheEntry.cs index 3b91de9..09e7fac 100644 --- a/src/HttpClient.Cache/InMemory/MemoryCacheEntry.cs +++ b/src/HttpClient.Cache/InMemory/MemoryCacheEntry.cs @@ -82,9 +82,14 @@ public IList ExpirationTokens } } - public IList PostEvictionCallbacks => - _postEvictionCallbacks ?? new List(); - + public IList PostEvictionCallbacks + { + get + { + return _postEvictionCallbacks ??= new List(); + } + } + internal DateTimeOffset LastAccessed { get; set; } internal EvictionReason EvictionReason { get; private set; } diff --git a/tests/HttpClient.Cache.Tests/InMemory/MemoryCacheEntryExtensionsTests.cs b/tests/HttpClient.Cache.Tests/InMemory/MemoryCacheEntryExtensionsTests.cs new file mode 100644 index 0000000..0bf856f --- /dev/null +++ b/tests/HttpClient.Cache.Tests/InMemory/MemoryCacheEntryExtensionsTests.cs @@ -0,0 +1,65 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using HttpClient.Cache.InMemory; + +namespace HttpClient.Cache.Tests.InMemory; + +public class MemoryCacheEntryExtensionsTests +{ + [Fact] + public void SetOptions_SetOptionsToMemoryCacheEntry_OptionsAreSet() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + TestChangeToken changeToken = new("cache-entry"); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + entry.SetOptions(new MemoryCacheEntryOptions + { + Priority = CacheEntryPriority.High, + AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1), + AbsoluteExpiration = DateTimeOffset.Now.AddDays(2), + SlidingExpiration = TimeSpan.FromMinutes(1), + ExpirationTokens = { changeToken }, + PostEvictionCallbacks = + { + new PostEvictionCallbackRegistration((key, value, reason, state) => { }, null) + } + }); + + using (new AssertionScope()) + { + entry.AbsoluteExpiration.Should().BeCloseTo(DateTimeOffset.Now.AddDays(2), TimeSpan.FromSeconds(1)); + entry.AbsoluteExpirationRelativeToNow.Should().BeCloseTo(TimeSpan.FromDays(1), TimeSpan.FromSeconds(1)); + entry.SlidingExpiration.Should().BeCloseTo(TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1)); + entry.Priority.Should().Be(CacheEntryPriority.High); + entry.ExpirationTokens.Should().ContainSingle(item => item.Equals(changeToken)); + entry.PostEvictionCallbacks.Should().HaveCount(1); + } + } + + private class TestChangeToken : IChangeToken + { + private readonly string _resource; + + public TestChangeToken(string resource) + { + _resource = resource; + } + + public bool HasChanged => false; + public bool ActiveChangeCallbacks => true; + + public IDisposable RegisterChangeCallback(Action callback, object state) + { + return new DisposableResource(); + } + } + + private class DisposableResource : IDisposable + { + public void Dispose() + { + } + } +} \ No newline at end of file From 27a6a3f37a597ff15842c81f5d66d77f08df86b3 Mon Sep 17 00:00:00 2001 From: Leefrost Date: Wed, 19 Apr 2023 22:32:12 +0300 Subject: [PATCH 5/6] Update tests for entry extensions --- src/HttpClient.Cache/CacheEntryExtensions.cs | 6 +- .../InMemory/InMemoryCacheHandlerTests.cs | 2 +- .../MemoryCacheEntryExtensionsTests.cs | 110 ++++++++++++++++++ ...stHandler.cs => TestHttpMessageHandler.cs} | 10 +- 4 files changed, 119 insertions(+), 9 deletions(-) rename tests/HttpClient.Cache.Tests/{TestHandler.cs => TestHttpMessageHandler.cs} (80%) diff --git a/src/HttpClient.Cache/CacheEntryExtensions.cs b/src/HttpClient.Cache/CacheEntryExtensions.cs index 279f3fa..bfa26d3 100644 --- a/src/HttpClient.Cache/CacheEntryExtensions.cs +++ b/src/HttpClient.Cache/CacheEntryExtensions.cs @@ -32,7 +32,7 @@ public static ICacheEntry SetSlidingExpiration(this ICacheEntry entry, TimeSpan return entry; } - public static ICacheEntry AddExpirationToken(this ICacheEntry entry, IChangeToken token) + public static ICacheEntry AddExpirationToken(this ICacheEntry entry, IChangeToken? token) { if (token == null) { @@ -43,8 +43,8 @@ public static ICacheEntry AddExpirationToken(this ICacheEntry entry, IChangeToke return entry; } - public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, PostEvictionDelegate callback, - object state) + public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, PostEvictionDelegate? callback, + object? state) { if (callback == null) { diff --git a/tests/HttpClient.Cache.Tests/InMemory/InMemoryCacheHandlerTests.cs b/tests/HttpClient.Cache.Tests/InMemory/InMemoryCacheHandlerTests.cs index 8cc86d3..19ce204 100644 --- a/tests/HttpClient.Cache.Tests/InMemory/InMemoryCacheHandlerTests.cs +++ b/tests/HttpClient.Cache.Tests/InMemory/InMemoryCacheHandlerTests.cs @@ -8,7 +8,7 @@ public class InMemoryCacheHandlerTests [Fact] public async Task Cache_HandleNewCacheMessage_Successful() { - var testHandler = new TestHandler(); + var testHandler = new TestHttpMessageHandler(); var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testHandler)); await client.GetAsync("http://the-url"); diff --git a/tests/HttpClient.Cache.Tests/InMemory/MemoryCacheEntryExtensionsTests.cs b/tests/HttpClient.Cache.Tests/InMemory/MemoryCacheEntryExtensionsTests.cs index 0bf856f..44f0d17 100644 --- a/tests/HttpClient.Cache.Tests/InMemory/MemoryCacheEntryExtensionsTests.cs +++ b/tests/HttpClient.Cache.Tests/InMemory/MemoryCacheEntryExtensionsTests.cs @@ -6,6 +6,116 @@ namespace HttpClient.Cache.Tests.InMemory; public class MemoryCacheEntryExtensionsTests { + [Fact] + public void SetValue_SetValueToMemoryCacheEntry_ValueIsSet() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + entry.SetValue("the-value"); + + entry.Value.Should().Be("the-value"); + } + + [Fact] + public void SetPriority_SetPriorityToMemoryCacheEntry_PriorityIsSet() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + entry.SetPriority(CacheEntryPriority.NeverRemove); + + entry.Priority.Should().Be(CacheEntryPriority.NeverRemove); + } + + [Fact] + public void SetAbsoluteExpiration_SetAbsoluteExpirationToMemoryCacheEntry_AbsoluteExpirationIsSet() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + entry.SetAbsoluteExpiration(DateTimeOffset.Now.AddDays(1)); + + entry.AbsoluteExpiration.Should().BeCloseTo(DateTimeOffset.Now.AddDays(1), TimeSpan.FromSeconds(1)); + } + + [Fact] + public void SetAbsoluteExpiration_SetAbsoluteExpirationRelativeToNowToMemoryCacheEntry_AbsoluteExpirationIsSet() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + entry.SetAbsoluteExpiration(TimeSpan.FromDays(1)); + + entry.AbsoluteExpirationRelativeToNow.Should().BeCloseTo(TimeSpan.FromDays(1), TimeSpan.FromSeconds(1)); + } + + [Fact] + public void SetSlidingExpiration_SetSlidingExpirationToMemoryCacheEntry_SlidingExpirationIsSet() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + entry.SetSlidingExpiration(TimeSpan.FromDays(1)); + + entry.SlidingExpiration.Should().BeCloseTo(TimeSpan.FromDays(1), TimeSpan.FromSeconds(1)); + } + + [Fact] + public void AddExpirationToken_AddExpirationTokenToMemoryCacheEntry_TokenIsSet() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + TestChangeToken changeToken = new("cache-entry"); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + entry.AddExpirationToken(changeToken); + + entry.ExpirationTokens.Should().ContainSingle(item => item.Equals(changeToken)); + } + + [Fact] + public void AddExpirationToken_AddNullToken_ThrowArgumentException() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + var action = () => entry.AddExpirationToken(null); + + action.Should().Throw(); + } + + [Fact] + public void RegisterPostEvictionCallback_RegisterPostEvictionCallbackToMemoryCacheEntry_CallbackIsSet() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + TestChangeToken changeToken = new("cache-entry"); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + entry.RegisterPostEvictionCallback((key, value, reason, state) => { }, null); + + entry.PostEvictionCallbacks.Should().HaveCount(1); + } + + [Fact] + public void RegisterPostEvictionCallback_AddNullCallback_ThrowArgumentException() + { + const string cacheKey = "key"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); + var action = () => entry.RegisterPostEvictionCallback(null, null); + + action.Should().Throw(); + } + [Fact] public void SetOptions_SetOptionsToMemoryCacheEntry_OptionsAreSet() { diff --git a/tests/HttpClient.Cache.Tests/TestHandler.cs b/tests/HttpClient.Cache.Tests/TestHttpMessageHandler.cs similarity index 80% rename from tests/HttpClient.Cache.Tests/TestHandler.cs rename to tests/HttpClient.Cache.Tests/TestHttpMessageHandler.cs index 61d8224..932c698 100644 --- a/tests/HttpClient.Cache.Tests/TestHandler.cs +++ b/tests/HttpClient.Cache.Tests/TestHttpMessageHandler.cs @@ -3,11 +3,11 @@ namespace HttpClient.Cache.Tests; -public class TestHandler : HttpMessageHandler +internal class TestHttpMessageHandler : HttpMessageHandler { - internal const HttpStatusCode DefaultCode = HttpStatusCode.OK; - internal const string DefaultContent = "The response content"; - internal const string DefaultContentType = "text/plain"; + private const HttpStatusCode DefaultCode = HttpStatusCode.OK; + private const string DefaultContent = "The response content"; + private const string DefaultContentType = "text/plain"; private readonly string _content; private readonly string _contentType; @@ -16,7 +16,7 @@ public class TestHandler : HttpMessageHandler private readonly HttpStatusCode _responseCode; - public TestHandler( + public TestHttpMessageHandler( HttpStatusCode responseCode = DefaultCode, string content = DefaultContent, string contentType = DefaultContentType, From 2b040edf4bc85454c75e92367dba2caeb55f690f Mon Sep 17 00:00:00 2001 From: Leefrost Date: Wed, 19 Apr 2023 22:47:37 +0300 Subject: [PATCH 6/6] Added architecture view --- HttpClient.Cache.graphml | 1671 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1671 insertions(+) create mode 100644 HttpClient.Cache.graphml diff --git a/HttpClient.Cache.graphml b/HttpClient.Cache.graphml new file mode 100644 index 0000000..6dbffad --- /dev/null +++ b/HttpClient.Cache.graphml @@ -0,0 +1,1671 @@ + + + + + true + + + + + + + + false + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Organic + + + + 0.340152426795026 + 0 + None + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\Clock\DefaultSystemClock.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\Clock\ISystemClock.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\IMemoryCache.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\ICacheEntry.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\CacheEntryPriority.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\IChangeToken.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\PostEvictionCallbackRegistration.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\InMemoryCacheHandler.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\Stats\ICacheStatsProvider.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\ICacheKeysProvider.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\MemoryCache.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\MemoryCacheOptions.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\Stats\DefaultCacheStatsProvider.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\DefaultCacheKeysProvider.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\MemoryCacheExtensions.cs + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\Utils\HttpResponseMessageExtensions.cs + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\Utils\HttpStatusCodeExtensions.cs + + + true + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\Stats\CacheStatsReport.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\MemoryCacheEntry.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\EvictionReason.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\Stats\CacheStatsResult.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\CacheData.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\CacheDataExtensions.cs + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\PostEvictionCallbackRegistration.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\MemoryCacheEntryExtensions.cs + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\InMemory\MemoryCacheEntryOptions.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D:\Repositories\GitHub\HttpClient.Cache\src\HttpClient.Cache\CacheEntryExtensions.cs + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _responseCache + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _clock + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file