diff --git a/HttpClient.Caching/Abstractions/CacheExtensions.cs b/HttpClient.Caching/Abstractions/CacheExtensions.cs deleted file mode 100644 index 4e1c51e..0000000 --- a/HttpClient.Caching/Abstractions/CacheExtensions.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.InMemory; - -namespace Microsoft.Extensions.Caching.Abstractions -{ - public static class CacheExtensions - { - public static object? Get(this IMemoryCache cache, object key) - { - cache.TryGetValue(key, out var obj); - return obj; - } - - public static TItem? Get(this IMemoryCache cache, object key) - { - cache.TryGetValue(key, out TItem? obj); - return obj; - } - - public static bool TryGetValue(this IMemoryCache cache, object key, [NotNullWhen(true)] out TItem? value) - { - if (cache.TryGetValue(key, out var obj)) - { - value = (TItem)obj; - return true; - } - - value = default; - return false; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value) where TItem : notnull - { - var entry = cache.CreateEntry(key); - entry.Value = value; - entry.Dispose(); - return value; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value, DateTimeOffset absoluteExpiration) where TItem : notnull - { - var entry = cache.CreateEntry(key); - DateTimeOffset? nullable = absoluteExpiration; - entry.AbsoluteExpiration = nullable; - entry.Value = value; - entry.Dispose(); - return value; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value, TimeSpan absoluteExpirationRelativeToNow) where TItem : notnull - { - var entry = cache.CreateEntry(key); - TimeSpan? nullable = absoluteExpirationRelativeToNow; - entry.AbsoluteExpirationRelativeToNow = nullable; - entry.Value = value; - entry.Dispose(); - return value; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value, IChangeToken expirationToken) where TItem : notnull - { - var entry = cache.CreateEntry(key); - var expirationToken1 = expirationToken; - entry.AddExpirationToken(expirationToken1); - entry.Value = value; - entry.Dispose(); - return value; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options) where TItem : notnull - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - using (var entry = cache.CreateEntry(key)) - { - entry.SetOptions(options); - entry.Value = value; - } - - return value; - } - - public static TItem GetOrCreate(this IMemoryCache cache, object key, Func factory) where TItem : notnull - { - if (!cache.TryGetValue(key, out var obj)) - { - var entry = cache.CreateEntry(key); - obj = factory(entry); - entry.SetValue(obj); - entry.Dispose(); - } - - return (TItem)obj; - } - - public static async Task GetOrCreateAsync(this IMemoryCache cache, object key, Func> factory) where TItem : notnull - { - if (!cache.TryGetValue(key, out var obj)) - { - var entry = cache.CreateEntry(key); - obj = await factory(entry); - entry.SetValue(obj); - entry.Dispose(); - entry = null; - } - - return (TItem)obj; - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/CacheItemPriority.cs b/HttpClient.Caching/Abstractions/CacheItemPriority.cs deleted file mode 100644 index d4a06bf..0000000 --- a/HttpClient.Caching/Abstractions/CacheItemPriority.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.Extensions.Caching.Abstractions -{ - /// - /// Specifies how items are prioritized for preservation during a memory pressure triggered cleanup. - /// - public enum CacheItemPriority - { - Low, - Normal, - High, - NeverRemove, - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/EvictionReason.cs b/HttpClient.Caching/Abstractions/EvictionReason.cs deleted file mode 100644 index 1283c21..0000000 --- a/HttpClient.Caching/Abstractions/EvictionReason.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Microsoft.Extensions.Caching.Abstractions -{ - public enum EvictionReason - { - None = 0, - Removed = 1, - Replaced = 2, - Expired = 3, - TokenExpired = 4, - Capacity = 5, - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/ICacheEntry.cs b/HttpClient.Caching/Abstractions/ICacheEntry.cs deleted file mode 100644 index 87ea776..0000000 --- a/HttpClient.Caching/Abstractions/ICacheEntry.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Microsoft.Extensions.Caching.Abstractions -{ - /// - /// Represents an entry in the implementation. - /// - public interface ICacheEntry : IDisposable - { - /// Gets the key of the cache entry. - object Key { get; } - - /// Gets or set the value of the cache entry. - object Value { get; set; } - - /// - /// Gets or sets an absolute expiration date for the cache entry. - /// - DateTimeOffset? AbsoluteExpiration { get; set; } - - /// - /// Gets or sets an absolute expiration time, relative to now. - /// - TimeSpan? AbsoluteExpirationRelativeToNow { get; set; } - - /// - /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed. - /// This will not extend the entry lifetime beyond the absolute expiration (if set). - /// - TimeSpan? SlidingExpiration { get; set; } - - /// - /// Gets the instances which cause the cache - /// entry to expire. - /// - IList ExpirationTokens { get; } - - /// - /// Gets or sets the callbacks will be fired after the cache entry is evicted from the cache. - /// - IList PostEvictionCallbacks { get; } - - /// - /// Gets or sets the priority for keeping the cache entry in the cache during a - /// memory pressure triggered cleanup. The default is - /// . - /// - CacheItemPriority Priority { get; set; } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/ICacheKeysProvider.cs b/HttpClient.Caching/Abstractions/ICacheKeysProvider.cs index 34c4e2a..372df7d 100644 --- a/HttpClient.Caching/Abstractions/ICacheKeysProvider.cs +++ b/HttpClient.Caching/Abstractions/ICacheKeysProvider.cs @@ -10,8 +10,8 @@ public interface ICacheKeysProvider /// /// Return the key for the request message /// - /// - /// + /// The http request message. + /// The cache key. string GetKey(HttpRequestMessage request); } } diff --git a/HttpClient.Caching/Abstractions/IChangeToken.cs b/HttpClient.Caching/Abstractions/IChangeToken.cs deleted file mode 100644 index 39b4bc8..0000000 --- a/HttpClient.Caching/Abstractions/IChangeToken.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Microsoft.Extensions.Caching.Abstractions -{ - /// Propagates notifications that a change has occured. - public interface IChangeToken - { - /// Gets a value that indicates if a change has occured. - bool HasChanged { get; } - - /// - /// Indicates if this token will pro-actively raise callbacks. Callbacks are still guaranteed to fire, eventually. - /// - bool ActiveChangeCallbacks { get; } - - /// - /// Registers for a callback that will be invoked when the entry has changed. - /// MUST be set before the callback - /// is invoked. - /// - /// The to invoke. - /// State to be passed into the callback. - /// An that is used to unregister the callback. - IDisposable RegisterChangeCallback(Action callback, object state); - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs b/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs deleted file mode 100644 index 29985ee..0000000 --- a/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Microsoft.Extensions.Caching.Abstractions -{ - public class PostEvictionCallbackRegistration - { - public PostEvictionDelegate EvictionCallback { get; set; } = null!; - - public object? State { get; set; } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs b/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs deleted file mode 100644 index c6718fb..0000000 --- a/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.Extensions.Caching.Abstractions -{ - /// - /// Signature of the callback which gets called when a cache entry expires. - /// - /// - /// - /// The . - /// The information that was passed when registering the callback. - public delegate void PostEvictionDelegate(object key, object value, EvictionReason evictionReason, object? state); -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/StatusCodeExtensions.cs b/HttpClient.Caching/Abstractions/StatusCodeExtensions.cs index ce1e1bb..2eae74d 100644 --- a/HttpClient.Caching/Abstractions/StatusCodeExtensions.cs +++ b/HttpClient.Caching/Abstractions/StatusCodeExtensions.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Net; namespace Microsoft.Extensions.Caching.Abstractions diff --git a/HttpClient.Caching/HttpClient.Caching.csproj b/HttpClient.Caching/HttpClient.Caching.csproj index 80df6e8..fb98019 100644 --- a/HttpClient.Caching/HttpClient.Caching.csproj +++ b/HttpClient.Caching/HttpClient.Caching.csproj @@ -1,7 +1,7 @@  - net48;netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + net462;netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 Library Microsoft.Extensions.Caching enable @@ -43,13 +43,14 @@ + true - + diff --git a/HttpClient.Caching/InMemory/CacheEntry.cs b/HttpClient.Caching/InMemory/CacheEntry.cs deleted file mode 100644 index 08ce5b9..0000000 --- a/HttpClient.Caching/InMemory/CacheEntry.cs +++ /dev/null @@ -1,318 +0,0 @@ -using System.Diagnostics; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.InMemory -{ - internal class CacheEntry : ICacheEntry - { - private static readonly Action ExpirationCallback = ExpirationTokensExpired; - internal readonly object Lock = new object(); - private bool added; - private readonly Action notifyCacheOfExpiration; - private readonly Action notifyCacheEntryDisposed; - private IList? expirationTokenRegistrations; - private IList? postEvictionCallbacks; - private bool isExpired; - internal IList? expirationTokens; - internal DateTimeOffset? absoluteExpiration; - internal TimeSpan? absoluteExpirationRelativeToNow; - private TimeSpan? slidingExpiration; - - public CacheItemPriority Priority { get; set; } = CacheItemPriority.Normal; - - public DateTimeOffset? AbsoluteExpiration - { - get { return this.absoluteExpiration; } - set { this.absoluteExpiration = value; } - } - - public TimeSpan? AbsoluteExpirationRelativeToNow - { - get { return this.absoluteExpirationRelativeToNow; } - set - { - var nullable = value; - if ((nullable.HasValue ? (nullable.GetValueOrDefault() <= TimeSpan.Zero ? 1 : 0) : 0) != 0) - { - throw new ArgumentOutOfRangeException(nameof(this.AbsoluteExpirationRelativeToNow), value, "The relative expiration value must be positive."); - } - - this.absoluteExpirationRelativeToNow = value; - } - } - - public TimeSpan? SlidingExpiration - { - get { return this.slidingExpiration; } - set - { - var nullable = value; - if ((nullable.HasValue ? (nullable.GetValueOrDefault() <= TimeSpan.Zero ? 1 : 0) : 0) != 0) - { - throw new ArgumentOutOfRangeException(nameof(this.SlidingExpiration), value, "The sliding expiration value must be positive."); - } - - this.slidingExpiration = value; - } - } - - public IList ExpirationTokens - { - get - { - if (this.expirationTokens == null) - { - this.expirationTokens = new List(); - } - - return this.expirationTokens; - } - } - - public IList PostEvictionCallbacks - { - get - { - if (this.postEvictionCallbacks == null) - { - this.postEvictionCallbacks = new List(); - } - - return this.postEvictionCallbacks; - } - } - - public object Key { get; private set; } - - public object Value { get; set; } - - internal DateTimeOffset LastAccessed { get; set; } - - internal EvictionReason EvictionReason { get; private set; } - - internal CacheEntry(object key, Action notifyCacheEntryDisposed, Action notifyCacheOfExpiration) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if (notifyCacheEntryDisposed == null) - { - throw new ArgumentNullException(nameof(notifyCacheEntryDisposed)); - } - - if (notifyCacheOfExpiration == null) - { - throw new ArgumentNullException(nameof(notifyCacheOfExpiration)); - } - - this.Key = key; - this.notifyCacheEntryDisposed = notifyCacheEntryDisposed; - this.notifyCacheOfExpiration = notifyCacheOfExpiration; - } - - public void Dispose() - { - if (this.added) - { - return; - } - - this.added = true; - this.notifyCacheEntryDisposed(this); - //this.PropagateOptions(_added); - } - - internal bool CheckExpired(DateTimeOffset now) - { - if (!this.isExpired && !this.CheckForExpiredTime(now)) - { - return this.CheckForExpiredTokens(); - } - - return true; - } - - internal void SetExpired(EvictionReason reason) - { - this.EvictionReason = reason; - this.isExpired = true; - this.DetachTokens(); - } - - private bool CheckForExpiredTime(DateTimeOffset now) - { - if (this.absoluteExpiration.HasValue && this.absoluteExpiration.Value <= now) - { - this.SetExpired(EvictionReason.Expired); - return true; - } - - if (this.slidingExpiration.HasValue) - { - var timeSpan = now - this.LastAccessed; - var slidingExpiration = this.slidingExpiration; - if ((slidingExpiration.HasValue ? (timeSpan >= slidingExpiration.GetValueOrDefault() ? 1 : 0) : 0) != 0) - { - this.SetExpired(EvictionReason.Expired); - return true; - } - } - - return false; - } - - internal bool CheckForExpiredTokens() - { - if (this.expirationTokens != null) - { - for (var index = 0; index < this.expirationTokens.Count; ++index) - { - if (this.expirationTokens[index].HasChanged) - { - this.SetExpired(EvictionReason.TokenExpired); - return true; - } - } - } - - return false; - } - - internal void AttachTokens() - { - if (this.expirationTokens == null) - { - return; - } - - lock (this.Lock) - { - for (var i = 0; i < this.expirationTokens.Count; ++i) - { - var expirationToken = this.expirationTokens[i]; - if (expirationToken.ActiveChangeCallbacks) - { - if (this.expirationTokenRegistrations == null) - { - this.expirationTokenRegistrations = new List(1); - } - - this.expirationTokenRegistrations.Add(expirationToken.RegisterChangeCallback(ExpirationCallback, this)); - } - } - } - } - - private static void ExpirationTokensExpired(object obj) - { - Task.Factory.StartNew(state => - { - var cacheEntry = (CacheEntry)state; - cacheEntry.SetExpired(EvictionReason.TokenExpired); - cacheEntry.notifyCacheOfExpiration(cacheEntry); - }, obj, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - } - - private void DetachTokens() - { - lock (this.Lock) - { - var tokenRegistrations = this.expirationTokenRegistrations; - if (tokenRegistrations == null) - { - return; - } - - this.expirationTokenRegistrations = null; - foreach (var disposable in tokenRegistrations) - { - disposable.Dispose(); - } - } - } - - internal void InvokeEvictionCallbacks() - { - if (this.postEvictionCallbacks == null) - { - return; - } - - var factory = Task.Factory; - var none = CancellationToken.None; - var scheduler = TaskScheduler.Default; - factory.StartNew(state => InvokeCallbacks((CacheEntry)state), this, none, TaskCreationOptions.DenyChildAttach, scheduler); - } - - private static void InvokeCallbacks(CacheEntry entry) - { - var callbackRegistrationList = Interlocked.Exchange(ref entry.postEvictionCallbacks, null); - if (callbackRegistrationList == null) - { - return; - } - - foreach (var callbackRegistration in callbackRegistrationList) - { - try - { - var evictionCallback = callbackRegistration.EvictionCallback; - var key = entry.Key; - var obj = entry.Value; - var evictionReason = entry.EvictionReason; - var state = callbackRegistration.State; - evictionCallback.Invoke(key, obj, evictionReason, state); - } - catch (Exception ex) - { - Debug.WriteLine($"{ex}"); - } - } - } - - internal void PropagateOptions(CacheEntry? parent) - { - if (parent == null) - { - return; - } - - if (this.expirationTokens != null) - { - lock (this.Lock) - { - lock (parent.Lock) - { - using (var changeTokenEnumerator = this.expirationTokens.GetEnumerator()) - { - while (changeTokenEnumerator.MoveNext()) - { - var changeToken = changeTokenEnumerator.Current; - parent.AddExpirationToken(changeToken); - } - } - } - } - } - - if (!this.absoluteExpiration.HasValue) - { - return; - } - - if (parent.absoluteExpiration.HasValue) - { - var absoluteExpiration = this.absoluteExpiration; - var parentAbsoluteExpiration = parent.absoluteExpiration; - if ((absoluteExpiration.HasValue & parentAbsoluteExpiration.HasValue ? (absoluteExpiration.GetValueOrDefault() < parentAbsoluteExpiration.GetValueOrDefault() ? 1 : 0) : 0) == 0) - { - return; - } - } - - parent.absoluteExpiration = this.absoluteExpiration; - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/CacheEntryExtensions.cs b/HttpClient.Caching/InMemory/CacheEntryExtensions.cs deleted file mode 100644 index b56fdcd..0000000 --- a/HttpClient.Caching/InMemory/CacheEntryExtensions.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.InMemory -{ - public static class CacheEntryExtensions - { - /// - /// Sets the priority for keeping the cache entry in the cache during a memory pressure tokened cleanup. - /// - /// - /// - public static ICacheEntry SetPriority(this ICacheEntry entry, CacheItemPriority priority) - { - entry.Priority = priority; - return entry; - } - - /// - /// Expire the cache entry if the given - /// expires. - /// - /// The . - /// - /// The that causes the cache - /// entry to expire. - /// - public static ICacheEntry AddExpirationToken(this ICacheEntry entry, IChangeToken expirationToken) - { - if (expirationToken == null) - { - throw new ArgumentNullException(nameof(expirationToken)); - } - - entry.ExpirationTokens.Add(expirationToken); - return entry; - } - - /// Sets an absolute expiration time, relative to now. - /// - /// - public static ICacheEntry SetAbsoluteExpiration(this ICacheEntry entry, TimeSpan relative) - { - entry.AbsoluteExpirationRelativeToNow = relative; - return entry; - } - - /// Sets an absolute expiration date for the cache entry. - /// - /// - public static ICacheEntry SetAbsoluteExpiration(this ICacheEntry entry, DateTimeOffset absolute) - { - entry.AbsoluteExpiration = absolute; - return entry; - } - - /// - /// Sets how long the cache entry can be inactive (e.g. not accessed) before it will be removed. - /// This will not extend the entry lifetime beyond the absolute expiration (if set). - /// - /// - /// - public static ICacheEntry SetSlidingExpiration(this ICacheEntry entry, TimeSpan offset) - { - entry.SlidingExpiration = offset; - return entry; - } - - /// - /// The given callback will be fired after the cache entry is evicted from the cache. - /// - /// - /// - public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, PostEvictionDelegate callback) - { - if (callback == null) - { - throw new ArgumentNullException(nameof(callback)); - } - - return entry.RegisterPostEvictionCallback(callback, null); - } - - /// - /// The given callback will be fired after the cache entry is evicted from the cache. - /// - /// - /// - /// - public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, PostEvictionDelegate callback, object? state) - { - if (callback == null) - { - throw new ArgumentNullException(nameof(callback)); - } - - entry.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration { EvictionCallback = callback, State = state }); - return entry; - } - - /// Sets the value of the cache entry. - /// - /// - public static ICacheEntry SetValue(this ICacheEntry entry, object value) - { - entry.Value = value; - return entry; - } - - /// - /// Applies the values of an existing to - /// the entry. - /// - /// - /// - public static ICacheEntry SetOptions(this ICacheEntry entry, MemoryCacheEntryOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - entry.AbsoluteExpiration = options.AbsoluteExpiration; - entry.AbsoluteExpirationRelativeToNow = options.AbsoluteExpirationRelativeToNow; - entry.SlidingExpiration = options.SlidingExpiration; - entry.Priority = options.Priority; - foreach (var expirationToken in options.ExpirationTokens) - { - entry.AddExpirationToken(expirationToken); - } - - foreach (var evictionCallback in options.PostEvictionCallbacks) - { - entry.RegisterPostEvictionCallback(evictionCallback.EvictionCallback, evictionCallback.State); - } - - return entry; - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/IMemoryCache.cs b/HttpClient.Caching/InMemory/IMemoryCache.cs deleted file mode 100644 index 6793350..0000000 --- a/HttpClient.Caching/InMemory/IMemoryCache.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.InMemory -{ - /// - /// Represents a local in-memory cache whose values are not serialized. - /// - public interface IMemoryCache : IDisposable - { - int Count { get; } - - /// Gets the item associated with this key if present. - /// An object identifying the requested entry. - /// The located value or null. - /// True if the key was found. - bool TryGetValue(object key, [NotNullWhen(true)] out object? value); - - /// Create or overwrite an entry in the cache. - /// An object identifying the entry. - /// The newly created instance. - ICacheEntry CreateEntry(object key); - - /// Removes the object associated with the given key. - /// An object identifying the entry. - void Remove(object key); - - void Compact(double percentage); - - void Clear(); - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/Internal/ISystemClock.cs b/HttpClient.Caching/InMemory/Internal/ISystemClock.cs deleted file mode 100644 index 9b3e320..0000000 --- a/HttpClient.Caching/InMemory/Internal/ISystemClock.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Microsoft.Extensions.Caching.InMemory.Internal -{ - /// Abstracts the system clock to facilitate testing. - public interface ISystemClock - { - /// Retrieves the current system time in UTC. - DateTimeOffset UtcNow { get; } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/Internal/SystemClock.cs b/HttpClient.Caching/InMemory/Internal/SystemClock.cs deleted file mode 100644 index 3133403..0000000 --- a/HttpClient.Caching/InMemory/Internal/SystemClock.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.ComponentModel; - -namespace Microsoft.Extensions.Caching.InMemory.Internal -{ - /// Provides access to the normal system clock. - [EditorBrowsable(EditorBrowsableState.Never)] - public class SystemClock : ISystemClock - { - /// Retrieves the current system time in UTC. - public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/MemoryCache.cs b/HttpClient.Caching/InMemory/MemoryCache.cs deleted file mode 100644 index 37c277f..0000000 --- a/HttpClient.Caching/InMemory/MemoryCache.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.InMemory.Internal; - -namespace Microsoft.Extensions.Caching.InMemory -{ - public class MemoryCache : IMemoryCache - { - private readonly ConcurrentDictionary entries; - private readonly Action setEntry; - private readonly Action entryExpirationNotification; - private readonly ISystemClock clock; - private readonly TimeSpan expirationScanFrequency; - - private DateTimeOffset lastExpirationScan; - private bool disposed; - - public int Count => this.entries.Count; - - private ICollection> EntriesCollection => this.entries; - - public MemoryCache() : this(new MemoryCacheOptions()) - { - } - - public MemoryCache(MemoryCacheOptions memoryCacheOptions) - { - if (memoryCacheOptions == null) - { - throw new ArgumentNullException(nameof(memoryCacheOptions)); - } - - this.entries = new ConcurrentDictionary(); - this.setEntry = this.SetEntry; - this.entryExpirationNotification = this.EntryExpired; - this.clock = memoryCacheOptions.Clock ?? new SystemClock(); - this.expirationScanFrequency = memoryCacheOptions.ExpirationScanFrequency; - this.lastExpirationScan = this.clock.UtcNow; - } - - ~MemoryCache() - { - this.Dispose(false); - } - - public ICacheEntry CreateEntry(object key) - { - this.CheckDisposed(); - return new CacheEntry(key, this.setEntry, this.entryExpirationNotification); - } - - private void SetEntry(CacheEntry entry) - { - if (this.disposed) - { - return; - } - - var utcNow = this.clock.UtcNow; - var nullable = new DateTimeOffset?(); - if (entry.absoluteExpirationRelativeToNow.HasValue) - { - var dateTimeOffset = utcNow; - var expirationRelativeToNow = entry.absoluteExpirationRelativeToNow; - nullable = expirationRelativeToNow.HasValue ? dateTimeOffset + expirationRelativeToNow.GetValueOrDefault() : new DateTimeOffset?(); - } - else if (entry.absoluteExpiration.HasValue) - { - nullable = entry.absoluteExpiration; - } - - if (nullable.HasValue && (!entry.absoluteExpiration.HasValue || nullable.Value < entry.absoluteExpiration.Value)) - { - entry.absoluteExpiration = nullable; - } - - entry.LastAccessed = utcNow; - if (this.entries.TryGetValue(entry.Key, out var cacheEntry)) - { - cacheEntry.SetExpired(EvictionReason.Replaced); - } - - if (!entry.CheckExpired(utcNow)) - { - bool flag; - if (cacheEntry == null) - { - flag = this.entries.TryAdd(entry.Key, entry); - } - else - { - flag = this.entries.TryUpdate(entry.Key, entry, cacheEntry); - if (!flag) - { - flag = this.entries.TryAdd(entry.Key, entry); - } - } - - if (flag) - { - entry.AttachTokens(); - } - else - { - entry.SetExpired((EvictionReason)2); - entry.InvokeEvictionCallbacks(); - } - - cacheEntry?.InvokeEvictionCallbacks(); - } - else - { - entry.InvokeEvictionCallbacks(); - if (cacheEntry != null) - { - this.RemoveEntry(cacheEntry); - } - } - - this.StartScanForExpiredItems(); - } - - public bool TryGetValue(object key, [NotNullWhen(true)] out object? result) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - this.CheckDisposed(); - result = null; - var utcNow = this.clock.UtcNow; - var flag = false; - if (this.entries.TryGetValue(key, out var entry)) - { - if (entry.CheckExpired(utcNow) && entry.EvictionReason != EvictionReason.Replaced) - { - this.RemoveEntry(entry); - } - else - { - flag = true; - entry.LastAccessed = utcNow; - result = entry.Value; - //entry.PropagateOptions(CacheEntryHelper.Current); - } - } - - this.StartScanForExpiredItems(); - return flag; - } - - public void Remove(object key) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - this.CheckDisposed(); - if (this.entries.TryRemove(key, out var cacheEntry)) - { - cacheEntry.SetExpired(EvictionReason.Removed); - cacheEntry.InvokeEvictionCallbacks(); - } - - this.StartScanForExpiredItems(); - } - - public void Clear() - { - this.CheckDisposed(); - var keys = this.entries.Keys.ToList(); - foreach (var key in keys) - { - if (this.entries.TryRemove(key, out var cacheEntry)) - { - cacheEntry.SetExpired(EvictionReason.Removed); - cacheEntry.InvokeEvictionCallbacks(); - } - } - - this.StartScanForExpiredItems(); - } - - private void RemoveEntry(CacheEntry entry) - { - if (!this.EntriesCollection.Remove(new KeyValuePair(entry.Key, entry))) - { - return; - } - - entry.InvokeEvictionCallbacks(); - } - - private void EntryExpired(CacheEntry entry) - { - this.RemoveEntry(entry); - this.StartScanForExpiredItems(); - } - - private void StartScanForExpiredItems() - { - var utcNow = this.clock.UtcNow; - if (!(this.expirationScanFrequency < utcNow - this.lastExpirationScan)) - { - return; - } - - this.lastExpirationScan = utcNow; - var factory = Task.Factory; - var none = CancellationToken.None; - var scheduler = TaskScheduler.Default; - factory.StartNew(state => ScanForExpiredItems((MemoryCache)state!), this, none, TaskCreationOptions.DenyChildAttach, scheduler); - } - - private static void ScanForExpiredItems(MemoryCache cache) - { - var utcNow = cache.clock.UtcNow; - foreach (var entry in cache.entries.Values) - { - if (entry.CheckExpired(utcNow)) - { - cache.RemoveEntry(entry); - } - } - } - - public void Compact(double percentage) - { - var entriesToRemove = new List(); - var priorityEntries1 = new List(); - var priorityEntries2 = new List(); - var priorityEntries3 = new List(); - var utcNow = this.clock.UtcNow; - - foreach (var cacheEntry in this.entries.Values) - { - if (cacheEntry.CheckExpired(utcNow)) - { - entriesToRemove.Add(cacheEntry); - } - else - { - switch ((int)cacheEntry.Priority) - { - case 0: - priorityEntries1.Add(cacheEntry); - continue; - case 1: - priorityEntries2.Add(cacheEntry); - continue; - case 2: - priorityEntries3.Add(cacheEntry); - continue; - case 3: - continue; - default: - throw new NotSupportedException("Not implemented: " + cacheEntry.Priority); - } - } - } - - var removalCountTarget = (int)(this.entries.Count * percentage); - this.ExpirePriorityBucket(removalCountTarget, entriesToRemove, priorityEntries1); - this.ExpirePriorityBucket(removalCountTarget, entriesToRemove, priorityEntries2); - this.ExpirePriorityBucket(removalCountTarget, entriesToRemove, priorityEntries3); - foreach (var entry in entriesToRemove) - { - this.RemoveEntry(entry); - } - } - - private void ExpirePriorityBucket(int removalCountTarget, List entriesToRemove, List priorityEntries) - { - if (removalCountTarget <= entriesToRemove.Count) - { - return; - } - - if (entriesToRemove.Count + priorityEntries.Count <= removalCountTarget) - { - foreach (var priorityEntry in priorityEntries) - { - priorityEntry.SetExpired(EvictionReason.Capacity); - } - - entriesToRemove.AddRange(priorityEntries); - } - else - { - foreach (var cacheEntry in priorityEntries.OrderBy(entry => entry.LastAccessed)) - { - cacheEntry.SetExpired(EvictionReason.Capacity); - entriesToRemove.Add(cacheEntry); - if (removalCountTarget <= entriesToRemove.Count) - { - break; - } - } - } - } - - public void Dispose() - { - this.Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (this.disposed) - { - return; - } - - if (disposing) - { - GC.SuppressFinalize(this); - } - - this.disposed = true; - } - - private void CheckDisposed() - { - if (this.disposed) - { - throw new ObjectDisposedException(typeof(MemoryCache).FullName); - } - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs b/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs deleted file mode 100644 index 6f05f24..0000000 --- a/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.InMemory -{ - public class MemoryCacheEntryOptions - { - private TimeSpan? absoluteExpirationRelativeToNow; - private TimeSpan? slidingExpiration; - - /// - /// Gets the instances which cause the cache - /// entry to - /// expire. - /// - public IList ExpirationTokens { get; } = new List(); - - /// - /// Gets or sets the callbacks will be fired after the cache entry is evicted from the cache. - /// - public IList PostEvictionCallbacks { get; } = new List(); - - /// - /// Gets or sets the priority for keeping the cache entry in the cache during a - /// memory pressure triggered cleanup. The default is - /// . - /// - public CacheItemPriority Priority { get; set; } = CacheItemPriority.Normal; - - /// - /// Gets or sets an absolute expiration date for the cache entry. - /// - public DateTimeOffset? AbsoluteExpiration { get; set; } - - /// - /// Gets or sets an absolute expiration time, relative to now. - /// - public TimeSpan? AbsoluteExpirationRelativeToNow - { - get { return this.absoluteExpirationRelativeToNow; } - set - { - var nullable = value; - var zero = TimeSpan.Zero; - if ((nullable.HasValue ? (nullable.GetValueOrDefault() <= zero ? 1 : 0) : 0) != 0) - { - throw new ArgumentOutOfRangeException(nameof(this.AbsoluteExpirationRelativeToNow), value, "The relative expiration value must be positive."); - } - - this.absoluteExpirationRelativeToNow = value; - } - } - - /// - /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed. - /// This will not extend the entry lifetime beyond the absolute expiration (if set). - /// - public TimeSpan? SlidingExpiration - { - get => this.slidingExpiration; - set - { - var nullable = value; - var zero = TimeSpan.Zero; - if ((nullable.HasValue ? (nullable.GetValueOrDefault() <= zero ? 1 : 0) : 0) != 0) - { - throw new ArgumentOutOfRangeException(nameof(this.SlidingExpiration), value, "The sliding expiration value must be positive."); - } - - this.slidingExpiration = value; - } - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/MemoryCacheOptions.cs b/HttpClient.Caching/InMemory/MemoryCacheOptions.cs deleted file mode 100644 index 94c646b..0000000 --- a/HttpClient.Caching/InMemory/MemoryCacheOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.Extensions.Caching.InMemory.Internal; - -namespace Microsoft.Extensions.Caching.InMemory -{ - public class MemoryCacheOptions - { - public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromMinutes(1.0); - - public ISystemClock? Clock { get; set; } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Internals/Nullable.cs b/HttpClient.Caching/Internals/Nullable.cs index 8954a29..10a8f66 100644 --- a/HttpClient.Caching/Internals/Nullable.cs +++ b/HttpClient.Caching/Internals/Nullable.cs @@ -1,4 +1,4 @@ -#if NET48_OR_GREATER || NETSTANDARD1_2 || NETSTANDARD2_0 +#if NET462_OR_GREATER || NETSTANDARD1_2 || NETSTANDARD2_0 // ReSharper disable once CheckNamespace namespace System.Diagnostics.CodeAnalysis { diff --git a/HttpClient.Caching/InMemory/DefaultCacheKeysProvider.cs b/HttpClient.Caching/Memory/DefaultCacheKeysProvider.cs similarity index 92% rename from HttpClient.Caching/InMemory/DefaultCacheKeysProvider.cs rename to HttpClient.Caching/Memory/DefaultCacheKeysProvider.cs index 73c471b..d74997c 100644 --- a/HttpClient.Caching/InMemory/DefaultCacheKeysProvider.cs +++ b/HttpClient.Caching/Memory/DefaultCacheKeysProvider.cs @@ -1,9 +1,8 @@ -using System; -using System.Net.Http; +using System.Net.Http; using System.Text; using Microsoft.Extensions.Caching.Abstractions; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Provides keys to store or retrieve data in the cache in the default way (http method + http request Uri) diff --git a/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs b/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs similarity index 84% rename from HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs rename to HttpClient.Caching/Memory/IMemoryCacheExtensions.cs index cc2b328..495a009 100644 --- a/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs +++ b/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs @@ -1,25 +1,23 @@ -using System; -using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Caching.Abstractions; -using System.Diagnostics.CodeAnalysis; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Extension methods for an . /// internal static class IMemoryCacheExtensions { - public static bool TryGetCacheData(this IMemoryCache cache, string key, [NotNullWhen(true)] out CacheData? cacheData) + public static bool TryGetCacheData(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out CacheData? cacheData) { var result = false; cacheData = null; try { - if (cache.TryGetValue(key, out byte[]? binaryData)) + if (memoryCache.TryGetValue(key, out var binaryData)) { - cacheData = binaryData.Deserialize(); + cacheData = binaryData!.Deserialize(); result = true; } } @@ -71,4 +69,4 @@ public static bool TrySetCacheData(this IMemoryCache cache, string key, CacheDat return result; } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs b/HttpClient.Caching/Memory/InMemoryCacheFallbackHandler.cs similarity index 97% rename from HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs rename to HttpClient.Caching/Memory/InMemoryCacheFallbackHandler.cs index b291a75..155446b 100644 --- a/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs +++ b/HttpClient.Caching/Memory/InMemoryCacheFallbackHandler.cs @@ -1,10 +1,7 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using System.Net.Http; using Microsoft.Extensions.Caching.Abstractions; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Tries to retrieve the result from the HTTP call, and if it times out or results in an unsuccessful status code, @@ -137,4 +134,4 @@ protected override async Task SendAsync(HttpRequestMessage return null; } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs b/HttpClient.Caching/Memory/InMemoryCacheHandler.cs similarity index 99% rename from HttpClient.Caching/InMemory/InMemoryCacheHandler.cs rename to HttpClient.Caching/Memory/InMemoryCacheHandler.cs index fc34694..ba2e6be 100644 --- a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs +++ b/HttpClient.Caching/Memory/InMemoryCacheHandler.cs @@ -4,7 +4,7 @@ using System.Net.Http.Headers; using Microsoft.Extensions.Caching.Abstractions; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Tries to retrieve the result from an InMemory cache, and if that's not available, gets the value from the @@ -264,4 +264,4 @@ private bool TryGetCachedHttpResponseMessage(HttpRequestMessage request, string return false; } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs b/HttpClient.Caching/MethodUriHeadersCacheKeysProvider.cs similarity index 98% rename from HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs rename to HttpClient.Caching/MethodUriHeadersCacheKeysProvider.cs index 9efce4d..a4fa827 100644 --- a/HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs +++ b/HttpClient.Caching/MethodUriHeadersCacheKeysProvider.cs @@ -4,7 +4,7 @@ using System.Text; using Microsoft.Extensions.Caching.Abstractions; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Provides keys to store or retrieve data in the cache by using http method, specific headers and Uri diff --git a/README.md b/README.md index 07bcb27..2f00baf 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,19 @@ Use the following command to install HttpClient.Caching using NuGet package mana PM> Install-Package HttpClient.Caching -You can use this library in any .Net project which is compatible to .Net Framework 4.5+ and .Net Standard 1.2+ (e.g. Xamarin Android, iOS, Universal Windows Platform, etc.) +You can use this library in projects targeting `.NET Framework 4.6.2`, `.NET Standard 2.0`, `.NET 8.0` and later. ### The Purpose of HTTP Caching HTTP Caching affects both involved communication peers, the client and the server. On the server-side, caching is appropriate for improving throughput (scalability). HTTP caching doesn't make a single HTTP call faster but it can lead to better response performance in high-load scenarios. On the client-side, caching is used to avoid unnecessarily repetitiv HTTP calls. This leads to less waiting time on the client-side since cache reads have naturally a much better response performance than HTTP calls over relatively slow network links. ### API Usage #### Using MemoryCache -Declare IMemoryCache in your API service, either by creating an instance manually or by injecting IMemoryCache into your API service class. +Declare `IMemoryCache` in your API service, either by creating an instance manually or by injecting `IMemoryCache` into your API service class. ```C# -private readonly IMemoryCache memoryCache = new MemoryCache(); +private readonly IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); ``` -Following example show how IMemoryCache can be used to store an HTTP GET result in memory for a given time span (cacheExpirection): +Following example shows how `IMemoryCache` can be used to store an HTTP GET result in memory for a given time span (`cacheExpiration`): ```C# public async Task GetAsync(string uri, TimeSpan? cacheExpiration = null) { @@ -55,7 +55,7 @@ public async Task GetAsync(string uri, TimeSpan? cacheExpirati ``` #### Using InMemoryCacheHandler -HttpClient allows to inject a custom http handler. In the follwing example, we inject an HttpClientHandler which is nested into an InMemoryCacheHandler where the InMemoryCacheHandler is responsible for maintaining and reading the cache. +`HttpClient` allows injecting a custom HTTP handler. In the following example, an `HttpClientHandler` is nested into an `InMemoryCacheHandler`, and the `InMemoryCacheHandler` is responsible for maintaining and reading the cache. ```C# static void Main(string[] args) { @@ -110,9 +110,9 @@ TotalRequests: 5 ### Cache keys By default, requests will be cached by using a key which is composed with http method and url (only HEAD and GET http methods are supported). -If this default behavior isn't enough **you can implement your own ICacheKeyProvider** wich provides **cache key** starting **from HttpRequestMessage**. +If this default behavior isn't enough, you can implement your own `ICacheKeysProvider`, which builds a cache key from an `HttpRequestMessage`. -The following example show how use a cache provider of type MethodUriHeadersCacheKeysProvider. +The following example shows how to use a cache provider of type `MethodUriHeadersCacheKeysProvider`. This cache key provider is already implemented and evaluates http method, specified headers and url to compose a cache key. with InMemoryCacheHandler. ```C# @@ -122,7 +122,7 @@ static void Main(string[] args) var httpClientHandler = new HttpClientHandler(); var cacheExpirationPerHttpResponseCode = CacheExpirationProvider.CreateSimple(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5)); - // this is a CacheKeyProvider which evaluates http method, specified headers and url to compose a key + // this cache key provider evaluates http method, specified headers and url to compose a key var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "FIRST-HEADER", "SECOND-HEADER" }); var handler = new InMemoryCacheHandler( innerHandler: httpClientHandler, diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 2cf4915..d35f4af 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -1,5 +1,6 @@ 2.0 - Replace Newtonsoft.Json with System.Text.Json. +- Replace MemoryCache implementation with Microsoft.Extensions.Caching.Memory. - Drop support for netstandard 1.2. - Maintenance updates. diff --git a/Samples/ConsoleAppSample/Program.cs b/Samples/ConsoleAppSample/Program.cs index 4b25a2b..2ac76fc 100644 --- a/Samples/ConsoleAppSample/Program.cs +++ b/Samples/ConsoleAppSample/Program.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; namespace ConsoleAppSample { @@ -59,4 +59,4 @@ private static async Task Main(string[] args) Console.ReadKey(); } } -} \ No newline at end of file +} diff --git a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj index 42e8e0f..ab7d04f 100644 --- a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj +++ b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj @@ -1,7 +1,7 @@  - net48;net10.0 + net462;net10.0 enable latest enable @@ -15,7 +15,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,7 +23,7 @@ - + diff --git a/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs index 6be1772..6fda380 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs @@ -1,7 +1,7 @@ using System; using System.Net.Http; using FluentAssertions; -using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Xunit; namespace HttpClient.Caching.Tests.InMemory diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs index 9483887..beb0a7d 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs @@ -1,16 +1,15 @@ -using System; -using System.Net; +using System.Net; using System.Net.Http; -using System.Threading.Tasks; using FluentAssertions; using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Moq; using Xunit; namespace HttpClient.Caching.Tests.InMemory { + using HttpClient = System.Net.Http.HttpClient; + public class InMemoryCacheFallbackHandlerTests { private readonly string url = "http://unittest/"; @@ -18,31 +17,31 @@ public class InMemoryCacheFallbackHandlerTests [Fact] public async Task AlwaysCallsTheHttpHandler() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); - // execute twice + // Act twice await client.GetAsync(this.url); cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url).Should().NotBeNull(); // ensure it's cached before the 2nd call await client.GetAsync(this.url); - // validate + // Assert testMessageHandler.NumberOfCalls.Should().Be(2); } [Fact] public async Task AlwaysUpdatesTheCacheOnSuccess() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url)); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute twice, validate cache is called each time + // Act twice, validate cache is called each time await client.GetAsync(this.url); cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url), Times.Once); await client.GetAsync(this.url); @@ -52,15 +51,15 @@ public async Task AlwaysUpdatesTheCacheOnSuccess() [Fact] public async Task UpdatesTheCacheForHeadAndGetIndependently() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url)); cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + this.url)); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute twice, validate cache is called each time + // Act twice, validate cache is called each time await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, this.url)); await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, this.url)); cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + this.url), Times.Once); @@ -70,56 +69,56 @@ public async Task UpdatesTheCacheForHeadAndGetIndependently() [Fact] public async Task NeverUpdatesTheCacheOnFailure() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(HttpStatusCode.InternalServerError); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); - object expectedValue; + object? expectedValue; cache.Setup(c => c.CreateEntry(It.IsAny())); cache.Setup(c => c.TryGetValue(this.url, out expectedValue)).Returns(false); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute + // Act await client.GetAsync(this.url); - // validate + // Assert cache.Verify(c => c.CreateEntry(It.IsAny()), Times.Never); } [Fact] public async Task TriesToAccessCacheOnFailureButReturnsErrorIfNotInCache() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(HttpStatusCode.InternalServerError); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); - object expectedValue; + object? expectedValue; cache.Setup(c => c.TryGetValue(this.url, out expectedValue)).Returns(false); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute + // Act var result = await client.GetAsync(this.url); - // validate + // Assert result.StatusCode.Should().Be(HttpStatusCode.InternalServerError); } [Fact] public async Task GetsItFromTheHttpCallAfterBeingInCache() { - // setup + // Arrange var testMessageHandler1 = new TestMessageHandler(content: "message-1", delay: TimeSpan.FromMilliseconds(100)); var testMessageHandler2 = new TestMessageHandler(content: "message-2"); var cache = new MemoryCache(new MemoryCacheOptions()); - var client1 = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler1, TimeSpan.FromMilliseconds(1), TimeSpan.FromDays(1), null, cache)); - var client2 = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromMilliseconds(1), TimeSpan.FromDays(1), null, cache)); + var client1 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler1, TimeSpan.FromMilliseconds(1), TimeSpan.FromDays(1), null, cache)); + var client2 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromMilliseconds(1), TimeSpan.FromDays(1), null, cache)); - // execute twice + // Act twice var result1 = await client1.GetAsync(this.url); cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url).Should().NotBeNull(); var result2 = await client2.GetAsync(this.url); - // validate + // Assert // - that each message handler got called testMessageHandler1.NumberOfCalls.Should().Be(1); testMessageHandler2.NumberOfCalls.Should().Be(1); @@ -134,18 +133,18 @@ public async Task GetsItFromTheHttpCallAfterBeingInCache() [Fact] public async Task GetsItFromTheCacheWhenUnsuccessful() { - // setup + // Arrange var testMessageHandler1 = new TestMessageHandler(HttpStatusCode.OK, "message-1"); var testMessageHandler2 = new TestMessageHandler(HttpStatusCode.InternalServerError, "message-2"); var cache = new MemoryCache(new MemoryCacheOptions()); - var client1 = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler1, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); - var client2 = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); + var client1 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler1, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); + var client2 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); - // execute twice + // Act twice var result1 = await client1.GetAsync(this.url); var result2 = await client2.GetAsync(this.url); - // validate + // Assert // - that each message handler got called testMessageHandler1.NumberOfCalls.Should().Be(1); testMessageHandler2.NumberOfCalls.Should().Be(1); @@ -157,4 +156,4 @@ public async Task GetsItFromTheCacheWhenUnsuccessful() data2.Should().BeEquivalentTo(data1); } } -} \ No newline at end of file +} diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs index 1e61731..ca81267 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Net; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using System.Threading.Tasks; using FluentAssertions; using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Xunit; namespace HttpClient.Caching.Tests.InMemory @@ -414,6 +411,8 @@ public async Task ReturnsResponseHeader() var response = await client.GetAsync("http://unittest"); // Assert + response.Content.Headers.Should().NotBeNull(); + response.Content.Headers.ContentType.Should().NotBeNull(); response.Content.Headers.ContentType.MediaType.Should().Be("text/plain"); response.Content.Headers.ContentType.CharSet.Should().Be("utf-8"); } @@ -481,4 +480,4 @@ public async Task InvalidatesCachePerMethod() testMessageHandler.NumberOfCalls.Should().Be(3); } } -} \ No newline at end of file +} diff --git a/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs index 8dc5a3f..644c272 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs @@ -1,7 +1,7 @@ using System; using System.Net.Http; using FluentAssertions; -using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Xunit; namespace HttpClient.Caching.Tests.InMemory diff --git a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs index 0b04984..08bc32f 100644 --- a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs +++ b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs @@ -1,8 +1,6 @@ -using System; -using FluentAssertions; +using FluentAssertions; using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Xunit; using Xunit.Abstractions; @@ -39,4 +37,4 @@ public void ShouldSetCache() cache.Count.Should().Be(10); } } -} \ No newline at end of file +} diff --git a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs index 74797e6..5eec06c 100644 --- a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs +++ b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs @@ -1,10 +1,7 @@ -using System; -using System.Net; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace HttpClient.Caching.Tests.TestData { @@ -17,8 +14,8 @@ internal class TestMessageHandler : HttpMessageHandler private readonly HttpStatusCode responseStatusCode; private readonly string content; private readonly string contentType; - private readonly TimeSpan delay; - private readonly CacheControlHeaderValue cacheControl; + private readonly TimeSpan? delay; + private readonly CacheControlHeaderValue? cacheControl; private readonly Encoding encoding; public int NumberOfCalls { get; set; } @@ -27,9 +24,9 @@ public TestMessageHandler( HttpStatusCode responseStatusCode = DefaultResponseStatusCode, string content = DefaultContent, string contentType = DefaultContentType, - Encoding encoding = null, - TimeSpan delay = default, - CacheControlHeaderValue cacheControl = null) + Encoding? encoding = null, + TimeSpan? delay = null, + CacheControlHeaderValue? cacheControl = null) { this.responseStatusCode = responseStatusCode; this.content = content; @@ -59,9 +56,9 @@ protected override async Task SendAsync(HttpRequestMessage { this.NumberOfCalls++; - if (this.delay != default) + if (this.delay is TimeSpan delay) { - await Task.Delay(this.delay, cancellationToken); + await Task.Delay(delay, cancellationToken); } return this.CreateHttpResponseMessage(); @@ -72,9 +69,9 @@ protected override HttpResponseMessage Send(HttpRequestMessage request, Cancella { this.NumberOfCalls++; - if (this.delay != default) + if (this.delay is TimeSpan delay) { - Thread.Sleep(this.delay); + Thread.Sleep(delay); } return this.CreateHttpResponseMessage();