diff --git a/HttpClient.Caching.sln b/HttpClient.Caching.sln index f0c0010..a6b4236 100644 --- a/HttpClient.Caching.sln +++ b/HttpClient.Caching.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore azure-pipelines.yml = azure-pipelines.yml README.md = README.md + global.json = global.json + ReleaseNotes.txt = ReleaseNotes.txt EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppSample", "Samples\ConsoleAppSample\ConsoleAppSample.csproj", "{592B2324-79AA-4973-8CE5-F65BD503641F}" diff --git a/HttpClient.Caching/Abstractions/CacheData.cs b/HttpClient.Caching/Abstractions/CacheData.cs index 06d59a9..40f346b 100644 --- a/HttpClient.Caching/Abstractions/CacheData.cs +++ b/HttpClient.Caching/Abstractions/CacheData.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Net.Http; namespace Microsoft.Extensions.Caching.Abstractions diff --git a/HttpClient.Caching/Abstractions/CacheDataExtensions.cs b/HttpClient.Caching/Abstractions/CacheDataExtensions.cs index 5e1e016..479a626 100644 --- a/HttpClient.Caching/Abstractions/CacheDataExtensions.cs +++ b/HttpClient.Caching/Abstractions/CacheDataExtensions.cs @@ -1,32 +1,34 @@ -using System; -using Newtonsoft.Json; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Caching.Internals; namespace Microsoft.Extensions.Caching.Abstractions { public static class CacheDataExtensions { + private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new CacheDataJsonConverter() } + }; + public static byte[] Serialize(this CacheData cacheData) { - var json = JsonConvert.SerializeObject(cacheData); - var bytes = new byte[json.Length * sizeof(char)]; - Buffer.BlockCopy(json.ToCharArray(), 0, bytes, 0, bytes.Length); - return bytes; + return JsonSerializer.SerializeToUtf8Bytes(cacheData, SerializerOptions); } public static CacheData Deserialize(this byte[] cacheData) { try { - var chars = new char[cacheData.Length / sizeof(char)]; - Buffer.BlockCopy(cacheData, 0, chars, 0, cacheData.Length); - var json = new string(chars); - var data = JsonConvert.DeserializeObject(json); - return data; + return JsonSerializer.Deserialize(cacheData, SerializerOptions)!; } catch { - return null; + return null!; } } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/Abstractions/CacheExtensions.cs b/HttpClient.Caching/Abstractions/CacheExtensions.cs deleted file mode 100644 index 2df33f4..0000000 --- a/HttpClient.Caching/Abstractions/CacheExtensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -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, 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) - { - 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) - { - 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) - { - 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) - { - 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) - { - using (var entry = cache.CreateEntry(key)) - { - if (options != null) - { - entry.SetOptions(options); - } - - entry.Value = value; - } - - return value; - } - - public static TItem GetOrCreate(this IMemoryCache cache, object key, Func factory) - { - 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) - { - 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/HttpResponseMessageExtensions.cs b/HttpClient.Caching/Abstractions/HttpResponseMessageExtensions.cs index c50c0c3..a064ab1 100644 --- a/HttpClient.Caching/Abstractions/HttpResponseMessageExtensions.cs +++ b/HttpClient.Caching/Abstractions/HttpResponseMessageExtensions.cs @@ -16,7 +16,7 @@ public static async Task ToCacheEntryAsync(this HttpResponseMessage h return httpResponseMessage.ToCacheEntry(contentBytes); } -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER public static CacheData ToCacheEntry(this HttpResponseMessage httpResponseMessage) { using var contentStream = httpResponseMessage.Content.ReadAsStream(); 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 e1c438b..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; } - - 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 44905b2..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 reason, 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 07a42b5..fb98019 100644 --- a/HttpClient.Caching/HttpClient.Caching.csproj +++ b/HttpClient.Caching/HttpClient.Caching.csproj @@ -1,53 +1,56 @@  - - HttpClient.Caching adds http response caching to HttpClient. - HttpClient.Caching - 1.0.0 - 1.0.0 - Thomas Galliker - net48;netstandard1.2;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 - HttpClient.Caching + + net462;netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + Library Microsoft.Extensions.Caching - HttpClient.Caching - httpclient.caching;httpclient;caching;cache;inmemory + enable + latest + enable + true + True + + + + + HttpClient.Caching + HTTP response caching for HttpClient. + 1.0.0 + Thomas Galliker + HttpClient.Caching + httpclient.caching;httpclient;caching;cache;inmemory logo.png - README.md LICENSE - https://github.com/thomasgalliker/HttpClient.Caching - git - https://github.com/thomasgalliker/HttpClient.Caching - $(PackageTargetFallback);netcoreapp1.0 - 1.6.1 - True - latest - - - - superdev GmbH - HttpClient.Caching - false + README.md + https://github.com/thomasgalliker/HttpClient.Caching + git + https://github.com/thomasgalliker/HttpClient.Caching + superdev GmbH + false Copyright $([System.DateTime]::Now.ToString(`yyyy`)) © Thomas Galliker - -1.3 -- Add ICacheKeysProvider which allows to specify custom cache keys + $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../ReleaseNotes.txt")) + true + snupkg + true + true + -1.0 -- Initial release - - - - + - - - - - - - + + + + + + + true + + + + + diff --git a/HttpClient.Caching/InMemory/CacheEntry.cs b/HttpClient.Caching/InMemory/CacheEntry.cs deleted file mode 100644 index 804153c..0000000 --- a/HttpClient.Caching/InMemory/CacheEntry.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -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("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) - { - if (this.EvictionReason == null) - { - 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; - if (evictionCallback != null) - { - 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 3046896..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("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("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 9799319..0000000 --- a/HttpClient.Caching/InMemory/IMemoryCache.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -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, 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/IMemoryCacheExtensions.cs b/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs deleted file mode 100644 index b38c4e0..0000000 --- a/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.InMemory -{ - /// - /// Extension methods for an . - /// - internal static class IMemoryCacheExtensions - { - /// - /// Tries to get the data from cache, that is, ignoring all exceptions. - /// - /// The in memory cache. - /// The key to retrieve from the cache. - /// The data of the cache entry, or null if not found or on any error. - [Obsolete("Use TryGetCacheData")] - public static Task TryGetAsync(this IMemoryCache cache, string key) - { - try - { - if (cache.TryGetValue(key, out byte[] binaryData)) - { - return Task.FromResult(binaryData.Deserialize()); - } - - return Task.FromResult(default(CacheData)); - } - catch (Exception) - { - // ignore all exceptions; return null - return Task.FromResult(default(CacheData)); - } - } - - public static bool TryGetCacheData(this IMemoryCache cache, string key, out CacheData cacheData) - { - var result = false; - cacheData = default; - - try - { - if (cache.TryGetValue(key, out byte[] binaryData)) - { - cacheData = binaryData.Deserialize(); - result = true; - } - } - catch - { - // Ignore exception - } - - return result; - } - - /// - /// Tries to set a new value to the cache, that is, ignoring all exceptions. - /// - /// The in memory cache. - /// The key for this cache entry. - /// The value of this cache entry. - /// Expiration relative to now. - /// A task, when completed, has tried to put the entry into the cache. - public static Task TrySetAsync(this IMemoryCache cache, string key, CacheData value, TimeSpan absoluteExpirationRelativeToNow) - { - try - { - cache.Set(key, value.Serialize(), absoluteExpirationRelativeToNow); - return Task.FromResult(true); - } - catch (Exception) - { - // ignore all exceptions - return Task.FromResult(false); - } - } - - public static bool TrySetCacheData(this IMemoryCache cache, string key, CacheData value, TimeSpan absoluteExpirationRelativeToNow) - { - var result = false; - - try - { - cache.Set(key, value.Serialize(), absoluteExpirationRelativeToNow); - result = true; - } - catch - { - // Ignore exceptions - result = false; - } - - return result; - } - } -} \ 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 92517a9..0000000 --- a/HttpClient.Caching/InMemory/MemoryCache.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -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, 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 4b8f55a..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 { return 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 5c2316f..0000000 --- a/HttpClient.Caching/InMemory/MemoryCacheOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -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/CacheDataJsonConverter.cs b/HttpClient.Caching/Internals/CacheDataJsonConverter.cs new file mode 100644 index 0000000..721edf8 --- /dev/null +++ b/HttpClient.Caching/Internals/CacheDataJsonConverter.cs @@ -0,0 +1,89 @@ +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Caching.Abstractions; + +namespace Microsoft.Extensions.Caching.Internals +{ + internal sealed class CacheDataJsonConverter : JsonConverter + { + public override CacheData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + var root = document.RootElement; + + var data = root.TryGetProperty("data", out var dataElement) + ? dataElement.Deserialize(options) ?? Array.Empty() + : Array.Empty(); + + var reasonPhrase = root.TryGetProperty("reasonPhrase", out var reasonPhraseElement) + ? reasonPhraseElement.GetString() + : null; + + var statusCode = root.TryGetProperty("statusCode", out var statusCodeElement) + ? statusCodeElement.GetInt32() + : (int)HttpStatusCode.OK; + + var version = root.TryGetProperty("version", out var versionElement) + ? versionElement.GetString() + : null; + + var headers = root.TryGetProperty("headers", out var headersElement) + ? headersElement.Deserialize>(options) + : null; + + var contentHeaders = root.TryGetProperty("contentHeaders", out var contentHeadersElement) + ? contentHeadersElement.Deserialize>(options) + : null; + + var httpResponseMessage = new HttpResponseMessage { ReasonPhrase = reasonPhrase, StatusCode = (HttpStatusCode)statusCode, }; + + if (ParseVersion(version) is Version v) + { + httpResponseMessage.Version = v; + } + + return new CacheData( + data, + httpResponseMessage, + ConvertHeaders(headers), + ConvertHeaders(contentHeaders)); + } + + public override void Write(Utf8JsonWriter writer, CacheData value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteBase64String("data", value.Data); + writer.WriteString("reasonPhrase", value.CachableResponse.ReasonPhrase); + writer.WriteNumber("statusCode", (int)value.CachableResponse.StatusCode); + writer.WriteString("version", value.CachableResponse.Version?.ToString()); + + writer.WritePropertyName("headers"); + JsonSerializer.Serialize(writer, ConvertHeaders(value.Headers), options); + + writer.WritePropertyName("contentHeaders"); + JsonSerializer.Serialize(writer, ConvertHeaders(value.ContentHeaders), options); + + writer.WriteEndObject(); + } + + + private static Dictionary ConvertHeaders(Dictionary>? headers) + { + return headers?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()) ?? new Dictionary(); + } + + private static Dictionary> ConvertHeaders(Dictionary? headers) + { + return headers?.ToDictionary(kvp => kvp.Key, kvp => (IEnumerable)kvp.Value) ?? new Dictionary>(); + } + + private static Version? ParseVersion(string? version) + { + return string.IsNullOrWhiteSpace(version) + ? null + : Version.Parse(version); + } + } +} \ No newline at end of file diff --git a/HttpClient.Caching/Internals/Nullable.cs b/HttpClient.Caching/Internals/Nullable.cs new file mode 100644 index 0000000..10a8f66 --- /dev/null +++ b/HttpClient.Caching/Internals/Nullable.cs @@ -0,0 +1,148 @@ +#if NET462_OR_GREATER || NETSTANDARD1_2 || NETSTANDARD2_0 +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis +{ + /* + * Nullable Reference Types (NRT) are a compiler feature introduced with C# 8. + * Older target frameworks (.NET Framework 4.8, .NET Standard 2.0 and earlier) + * do not provide the nullable-analysis attributes that newer frameworks include. + * + * These attribute stubs are included only for those older TFMs so the compiler + * can emit correct nullability metadata and consumers can benefit from NRT + * annotations. They have no runtime behavior and should NOT be included when the + * framework already provides them (.NET Core 3.0+, .NET Standard 2.1+, .NET 5+). + */ + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class AllowNullAttribute : Attribute + { + public AllowNullAttribute() { } + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class DisallowNullAttribute : Attribute + { + public DisallowNullAttribute() { } + } + + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class DoesNotReturnAttribute : Attribute + { + public DoesNotReturnAttribute() { } + } + + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + public bool ParameterValue { get; } + + public DoesNotReturnIfAttribute(bool parameterValue) + { + this.ParameterValue = parameterValue; + } + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class MaybeNullAttribute : Attribute + { + public MaybeNullAttribute() { } + } + + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class MaybeNullWhenAttribute : Attribute + { + public bool ReturnValue { get; } + + public MaybeNullWhenAttribute(bool returnValue) + { + this.ReturnValue = returnValue; + } + } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class MemberNotNullAttribute : Attribute + { + public string[] Members { get; } + + public MemberNotNullAttribute(string member) + { + this.Members = new[] { member }; + } + + public MemberNotNullAttribute(params string[] members) + { + this.Members = members; + } + } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + public bool ReturnValue { get; } + + public string[] Members { get; } + + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + this.ReturnValue = returnValue; + this.Members = new[] { member }; + } + + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + this.ReturnValue = returnValue; + this.Members = members; + } + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class NotNullAttribute : Attribute + { + public NotNullAttribute() { } + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + AllowMultiple = true, + Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + public string ParameterName { get; } + + public NotNullIfNotNullAttribute(string parameterName) + { + this.ParameterName = parameterName; + } + } + + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class NotNullWhenAttribute : Attribute + { + public bool ReturnValue { get; } + + public NotNullWhenAttribute(bool returnValue) + { + this.ReturnValue = returnValue; + } + } + +#if NETSTANDARD1_2 + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + internal sealed class ExcludeFromCodeCoverage : Attribute + { + } +#endif +} +#endif 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/InMemoryCacheFallbackHandler.cs b/HttpClient.Caching/Memory/InMemoryCacheFallbackHandler.cs similarity index 90% rename from HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs rename to HttpClient.Caching/Memory/InMemoryCacheFallbackHandler.cs index 320e95e..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, @@ -28,7 +25,7 @@ public class InMemoryCacheFallbackHandler : DelegatingHandler /// An that records statistic information about the caching /// behavior. /// - public InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan maxTimeout, TimeSpan cacheDuration, IStatsProvider statsProvider = null) + public InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan maxTimeout, TimeSpan cacheDuration, IStatsProvider? statsProvider = null) : this(innerHandler, maxTimeout, cacheDuration, statsProvider, new MemoryCache(new MemoryCacheOptions())) { } @@ -44,7 +41,7 @@ public InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan ma /// behavior. /// /// The cache to be used. - internal InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan maxTimeout, TimeSpan cacheDuration, IStatsProvider statsProvider, IMemoryCache cache) : base(innerHandler ?? new HttpClientHandler()) + internal InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan maxTimeout, TimeSpan cacheDuration, IStatsProvider? statsProvider, IMemoryCache? cache) : base(innerHandler ?? new HttpClientHandler()) { this.StatsProvider = statsProvider ?? new StatsProvider(nameof(InMemoryCacheHandler)); this.maxTimeout = maxTimeout; @@ -64,7 +61,7 @@ protected override async Task SendAsync(HttpRequestMessage return await base.SendAsync(request, cancellationToken); } - var key = CacheFallbackKeyPrefix + request.Method + request.RequestUri.ToString(); + var key = $"{CacheFallbackKeyPrefix}{request.Method}{request.RequestUri}"; // start 3 tasks var httpSendTask = base.SendAsync(request, cancellationToken); @@ -111,9 +108,9 @@ protected override async Task SendAsync(HttpRequestMessage return response; } - private HttpResponseMessage ExtractCachedResponse(HttpRequestMessage request, string key) + private HttpResponseMessage? ExtractCachedResponse(HttpRequestMessage request, string key) { - // it's in the cache, return that result + // It's in the cache, return that result if (this.responseCache.TryGetCacheData(key, out var data)) { // get the data from the cache @@ -125,7 +122,7 @@ private HttpResponseMessage ExtractCachedResponse(HttpRequestMessage request, st return null; } - private async Task SaveToCache(HttpResponseMessage response, string key) + private async Task SaveToCache(HttpResponseMessage response, string key) { if ((int)response.StatusCode < 500 && TimeSpan.Zero != this.cacheDuration) { @@ -137,4 +134,4 @@ private async Task SaveToCache(HttpResponseMessage response, string k return null; } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs b/HttpClient.Caching/Memory/InMemoryCacheHandler.cs similarity index 90% rename from HttpClient.Caching/InMemory/InMemoryCacheHandler.cs rename to HttpClient.Caching/Memory/InMemoryCacheHandler.cs index 6ebf556..ba2e6be 100644 --- a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs +++ b/HttpClient.Caching/Memory/InMemoryCacheHandler.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; 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 @@ -15,7 +12,7 @@ namespace Microsoft.Extensions.Caching.InMemory /// public class InMemoryCacheHandler : DelegatingHandler { -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER /// /// The key to use to store the UseCache value in the HttpRequestMessage.Options dictionary. /// This key is used to determine if the cache should be checked for the request. @@ -66,10 +63,10 @@ public class InMemoryCacheHandler : DelegatingHandler /// /// An that provides keys to retrieve and store items in the cache /// - public InMemoryCacheHandler(HttpMessageHandler innerHandler = null, - IDictionary cacheExpirationPerHttpResponseCode = null, - IStatsProvider statsProvider = null, - ICacheKeysProvider cacheKeysProvider = null) + public InMemoryCacheHandler(HttpMessageHandler? innerHandler = null, + IDictionary? cacheExpirationPerHttpResponseCode = null, + IStatsProvider? statsProvider = null, + ICacheKeysProvider? cacheKeysProvider = null) : this(innerHandler, cacheExpirationPerHttpResponseCode, statsProvider, @@ -93,11 +90,11 @@ public InMemoryCacheHandler(HttpMessageHandler innerHandler = null, /// The cache to be used. /// The cache keys provider to use internal InMemoryCacheHandler( - HttpMessageHandler innerHandler, - IDictionary cacheExpirationPerHttpResponseCode, - IStatsProvider statsProvider, - IMemoryCache cache, - ICacheKeysProvider cacheKeysProvider) + HttpMessageHandler? innerHandler, + IDictionary? cacheExpirationPerHttpResponseCode, + IStatsProvider? statsProvider, + IMemoryCache? cache, + ICacheKeysProvider? cacheKeysProvider) : base(innerHandler ?? new HttpClientHandler()) { this.StatsProvider = statsProvider ?? new StatsProvider(nameof(InMemoryCacheHandler)); @@ -111,7 +108,7 @@ internal InMemoryCacheHandler( /// /// The URI to invalidate. /// An optional to invalidate. If none is provided, the cache is cleaned for all methods. - public void InvalidateCache(Uri uri, HttpMethod httpMethod = null) + public void InvalidateCache(Uri uri, HttpMethod? httpMethod = null) { var httpMethods = httpMethod != null ? new HashSet { httpMethod } @@ -132,7 +129,7 @@ public void InvalidateCache(Uri uri, HttpMethod httpMethod = null) /// A bool representing if the cache should be cached or not private static bool ShouldTheCacheBeChecked(HttpRequestMessage request) { -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER var useCacheOption = request.Options.TryGetValue(UseCache, out var useCache) == false || useCache == true; #else var useCacheOption = request.Properties.TryGetValue(UseCache, out var useCache) == false || (bool)useCache == true; @@ -165,7 +162,7 @@ private static bool ShouldCacheResponse(HttpResponseMessage response) /// The HttpResponseMessage from cache, or a newly invoked one. protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - string key = null; + string? key = null; // Gets the data from cache, and returns the data if it's a cache hit var isCachedHttpMethod = CachedHttpMethods.Contains(request.Method); @@ -199,7 +196,7 @@ protected override async Task SendAsync(HttpRequestMessage if (ShouldCacheResponse(response) && TimeSpan.Zero != maxCacheTime) { var entry = await response.ToCacheEntryAsync(); - await this.responseCache.TrySetAsync(key, entry, maxCacheTime); + await this.responseCache.TrySetAsync(key!, entry, maxCacheTime); return request.PrepareCachedEntry(entry); } } @@ -208,10 +205,10 @@ protected override async Task SendAsync(HttpRequestMessage return response; } -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { - string key = null; + string? key = null; // Gets the data from cache, and returns the data if it's a cache hit var isCachedHttpMethod = CachedHttpMethods.Contains(request.Method); @@ -244,7 +241,7 @@ protected override HttpResponseMessage Send(HttpRequestMessage request, Cancella if (ShouldCacheResponse(response) && TimeSpan.Zero != maxCacheTime) { var cacheData = response.ToCacheEntry(); - this.responseCache.TrySetCacheData(key, cacheData, maxCacheTime); + this.responseCache.TrySetCacheData(key!, cacheData, maxCacheTime); return request.PrepareCachedEntry(cacheData); } } @@ -254,7 +251,7 @@ protected override HttpResponseMessage Send(HttpRequestMessage request, Cancella } #endif - private bool TryGetCachedHttpResponseMessage(HttpRequestMessage request, string key, out HttpResponseMessage cachedResponse) + private bool TryGetCachedHttpResponseMessage(HttpRequestMessage request, string key, [NotNullWhen(true)] out HttpResponseMessage? cachedResponse) { if (this.responseCache.TryGetCacheData(key, out var cacheData)) { @@ -263,8 +260,8 @@ private bool TryGetCachedHttpResponseMessage(HttpRequestMessage request, string return true; } - cachedResponse = default; + cachedResponse = null; return false; } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/Memory/MemoryCacheExtensions.cs b/HttpClient.Caching/Memory/MemoryCacheExtensions.cs new file mode 100644 index 0000000..8ebd99c --- /dev/null +++ b/HttpClient.Caching/Memory/MemoryCacheExtensions.cs @@ -0,0 +1,114 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Caching.Abstractions; + +namespace Microsoft.Extensions.Caching.Memory +{ + public static class MemoryCacheExtensions + { + /// + /// Tries to get the value associated with the given key. + /// + /// The type of the object to get. + /// The instance this method extends. + /// The key of the value to get. + /// The value associated with the given key. + /// true if the key was found; false otherwise. + public static bool TryGetValue(this IMemoryCache cache, object key, [NotNullWhen(true)] out TItem? value) + { + if (cache.TryGetValue(key, out var result)) + { + if (result is TItem item) + { + value = item; + return true; + } + } + + value = default; + return false; + } + + /// + /// Clears all entries from the cache. + /// + /// The cache to clear. + /// Thrown when is null. + /// Thrown when the cache implementation cannot be cleared wholesale. + public static void Clear(this IMemoryCache memoryCache) + { + if (memoryCache is null) + { + throw new ArgumentNullException(nameof(memoryCache)); + } + + if (memoryCache is MemoryCache m) + { + m.Clear(); + return; + } + + throw new NotSupportedException($"Clear is not supported for cache type '{memoryCache.GetType().FullName}'."); + } + + internal static bool TryGetCacheData(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out CacheData? cacheData) + { + var result = false; + cacheData = null; + + try + { + if (TryGetValue(memoryCache, key, out var binaryData)) + { + cacheData = binaryData.Deserialize(); + result = true; + } + } + catch + { + // Ignore exception + } + + return result; + } + + /// + /// Tries to set a new value to the cache, that is, ignoring all exceptions. + /// + /// The in memory cache. + /// The key for this cache entry. + /// The value of this cache entry. + /// Expiration relative to now. + /// A task, when completed, has tried to put the entry into the cache. + internal static Task TrySetAsync(this IMemoryCache cache, string key, CacheData cacheData, TimeSpan absoluteExpirationRelativeToNow) + { + try + { + cache.Set(key, cacheData.Serialize(), absoluteExpirationRelativeToNow); + return Task.FromResult(true); + } + catch (Exception) + { + // ignore all exceptions + return Task.FromResult(false); + } + } + + internal static bool TrySetCacheData(this IMemoryCache cache, string key, CacheData value, TimeSpan absoluteExpirationRelativeToNow) + { + bool result; + + try + { + cache.Set(key, value.Serialize(), absoluteExpirationRelativeToNow); + result = true; + } + catch + { + // Ignore exceptions + result = false; + } + + return result; + } + } +} diff --git a/HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs b/HttpClient.Caching/MethodUriHeadersCacheKeysProvider.cs similarity index 89% rename from HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs rename to HttpClient.Caching/MethodUriHeadersCacheKeysProvider.cs index b172460..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 @@ -16,13 +16,10 @@ public class MethodUriHeadersCacheKeysProvider : ICacheKeysProvider /// /// Initialize the cache key provider passing the headers name that will be used to compose /// - /// - public MethodUriHeadersCacheKeysProvider(string[] headersName) + /// The header names to be included in the cache key. + public MethodUriHeadersCacheKeysProvider(string[]? headersName) { - if (headersName != null) - { - this.headersName = headersName.OrderBy(i => i).ToArray(); - } + this.headersName = headersName?.OrderBy(i => i).ToArray() ?? Array.Empty(); } /// diff --git a/LICENSE b/LICENSE index ae6ec5c..cddc001 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Thomas Galliker +Copyright (c) 2026 Thomas Galliker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 2aab6cd..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) { @@ -37,7 +37,7 @@ public async Task GetAsync(string uri, TimeSpan? cacheExpirati var httpResponseMessage = await this.HandleRequest(() => this.httpClient.GetAsync(uri)); var jsonResponse = await this.HandleResponse(httpResponseMessage); - result = await Task.Run(() => JsonConvert.DeserializeObject(jsonResponse, this.serializerSettings)); + result = await Task.Run(() => JsonSerializer.Deserialize(jsonResponse, this.serializerSettings)); if (caching) { @@ -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 new file mode 100644 index 0000000..d35f4af --- /dev/null +++ b/ReleaseNotes.txt @@ -0,0 +1,11 @@ +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. + +1.3 +- Add ICacheKeysProvider which allows to specify custom cache keys. + +1.0 +- Initial release. \ No newline at end of file diff --git a/Samples/ConsoleAppSample/ConsoleAppSample.csproj b/Samples/ConsoleAppSample/ConsoleAppSample.csproj index 908a3d3..c1f0fb2 100644 --- a/Samples/ConsoleAppSample/ConsoleAppSample.csproj +++ b/Samples/ConsoleAppSample/ConsoleAppSample.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 diff --git a/Samples/ConsoleAppSample/Program.cs b/Samples/ConsoleAppSample/Program.cs index 47a274e..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 { @@ -12,7 +12,7 @@ internal class Program { private static async Task Main(string[] args) { - const string url = "http://worldtimeapi.org/api/timezone/Europe/Zurich"; + const string url = "https://www.timeapi.io/api/v1/time/current/utc"; // HttpClient uses an HttpClientHandler nested into InMemoryCacheHandler in order to handle http get response caching var httpClientHandler = new HttpClientHandler(); @@ -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/Abstractions/CacheDataExtensionsTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs new file mode 100644 index 0000000..d9fea33 --- /dev/null +++ b/Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs @@ -0,0 +1,106 @@ +namespace HttpClient.Caching.Tests.Abstractions +{ + public class CacheDataExtensionsTests + { + [Fact] + public void SerializeAndDeserialize_RoundTripsCacheData() + { + // Arrange + var httpResponseMessage = new HttpResponseMessage + { + ReasonPhrase = "Accepted", + StatusCode = HttpStatusCode.Accepted, + Version = new Version(2, 0) + }; + + var cacheData = new CacheData( + new byte[] { 1, 2, 3 }, + httpResponseMessage, + new Dictionary> + { + ["X-Test"] = new[] { "one", "two" } + }, + new Dictionary> + { + ["Content-Type"] = new[] { "application/json" } + }); + + // Act + var serialized = cacheData.Serialize(); + var deserialized = serialized.Deserialize(); + + // Assert + deserialized.Should().NotBeNull(); + deserialized.Data.Should().Equal(1, 2, 3); + deserialized.CachableResponse.StatusCode.Should().Be(HttpStatusCode.Accepted); + deserialized.CachableResponse.ReasonPhrase.Should().Be("Accepted"); + deserialized.CachableResponse.Version.Should().Be(new Version(2, 0)); + deserialized.Headers.Should().ContainKey("X-Test"); + deserialized.Headers["X-Test"].Should().Equal("one", "two"); + deserialized.ContentHeaders.Should().ContainKey("Content-Type"); + deserialized.ContentHeaders["Content-Type"].Should().Equal("application/json"); + } + + [Fact] + public void Deserialize_InvalidPayload_ReturnsNull() + { + // Arrange + var bytes = new byte[] { 1, 2, 3 }; + + // Act + var deserialized = bytes.Deserialize(); + + // Assert + deserialized.Should().BeNull(); + } + + [Fact] + public void Deserialize_LegacyNewtonsoftPayload_ReturnsNull() + { + // Arrange + const string legacyJson = @"{ + ""CachableResponse"": { + ""Version"": ""2.0"", + ""Content"": { + ""Headers"": [] + }, + ""StatusCode"": 202, + ""ReasonPhrase"": ""Accepted"", + ""Headers"": [ + { + ""Key"": ""X-From-Response"", + ""Value"": [ + ""header-value"" + ] + } + ], + ""TrailingHeaders"": [], + ""RequestMessage"": null, + ""IsSuccessStatusCode"": true + }, + ""Data"": ""AQID"", + ""Headers"": { + ""X-Test"": [ + ""one"", + ""two"" + ] + }, + ""ContentHeaders"": { + ""Content-Type"": [ + ""application/json"" + ] + } +}"; + + var chars = legacyJson.ToCharArray(); + var legacyBytes = new byte[chars.Length * sizeof(char)]; + Buffer.BlockCopy(chars, 0, legacyBytes, 0, legacyBytes.Length); + + // Act + var deserialized = legacyBytes.Deserialize(); + + // Assert + deserialized.Should().BeNull(); + } + } +} diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/CacheExpirationProviderTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/CacheExpirationProviderTests.cs index 04109f4..628bcca 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/CacheExpirationProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/CacheExpirationProviderTests.cs @@ -1,10 +1,4 @@ -using System; -using System.Net; -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - -namespace HttpClient.Caching.Tests.Abstractions +namespace HttpClient.Caching.Tests.Abstractions { public class CacheExpirationProviderTests { diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/StatsProviderTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/StatsProviderTests.cs index a35a488..c16f974 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/StatsProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/StatsProviderTests.cs @@ -1,8 +1,3 @@ -using System.Net; -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - namespace HttpClient.Caching.Tests.Abstractions { public class StatsProviderTests diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/StatsResultTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/StatsResultTests.cs index 7dc2a5a..1ed809b 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/StatsResultTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/StatsResultTests.cs @@ -1,8 +1,3 @@ -using System.Net; -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - namespace HttpClient.Caching.Tests.Abstractions { public class StatsResultTests diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/StatsValueTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/StatsValueTests.cs index d1252f0..8bdd76b 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/StatsValueTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/StatsValueTests.cs @@ -1,7 +1,3 @@ -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - namespace HttpClient.Caching.Tests.Abstractions { public class StatsValueTests diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/StatusCodeExtensionsTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/StatusCodeExtensionsTests.cs index dc1170e..73f1fa5 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/StatusCodeExtensionsTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/StatusCodeExtensionsTests.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Net; -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - -namespace HttpClient.Caching.Tests.Abstractions +namespace HttpClient.Caching.Tests.Abstractions { public class StatusCodeExtensionsTests { diff --git a/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs b/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs new file mode 100644 index 0000000..4d29dbb --- /dev/null +++ b/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs @@ -0,0 +1,67 @@ +namespace HttpClient.Caching.Tests.Extensions +{ + public class MemoryCacheExtensionsTests + { + private readonly ITestOutputHelper testOutputHelper; + + public MemoryCacheExtensionsTests(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + } + + [Fact] + public void Clear_RemovesAllEntries() + { + // Arrange + var options = new MemoryCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromHours(1) + }; + var memoryCache = new MemoryCache(new MemoryCacheOptions()); + + for (var i = 1; i <= 10; i++) + { + memoryCache.Set($"{i}", new TestPayload(i), options); + } + + // Act + ((IMemoryCache)memoryCache).Clear(); + + // Assert + memoryCache.Count.Should().Be(0); + memoryCache.TryGetValue("1", out var result1).Should().BeFalse(); + result1.Should().BeNull(); + } + + [Fact] + public void TryGetValue_ReturnsTrueAndTypedValue() + { + // Arrange + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + memoryCache.Set("1", new TestPayload(1)); + + // Act + var result = MemoryCacheExtensions.TryGetValue(memoryCache, "1", out var value); + + // Assert + result.Should().BeTrue(); + value.Should().NotBeNull(); + value!.Id.Should().Be(1); + } + + [Fact] + public void TryGetValue_ReturnFalseAndNull() + { + // Arrange + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + memoryCache.Set("1", null); + + // Act + var result = MemoryCacheExtensions.TryGetValue(memoryCache, "1", out var value); + + // Assert + result.Should().BeFalse(); + value.Should().BeNull(); + } + } +} diff --git a/Tests/HttpClient.Caching.Tests/GlobalUsings.cs b/Tests/HttpClient.Caching.Tests/GlobalUsings.cs new file mode 100644 index 0000000..4916b11 --- /dev/null +++ b/Tests/HttpClient.Caching.Tests/GlobalUsings.cs @@ -0,0 +1,14 @@ +global using System; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.Net; +global using System.Net.Http; +global using System.Net.Http.Headers; +global using System.Text; +global using AwesomeAssertions; +global using HttpClient.Caching.Tests.TestData; +global using Microsoft.Extensions.Caching.Abstractions; +global using Microsoft.Extensions.Caching.Memory; +global using Moq; +global using Xunit; +global using Xunit.Abstractions; \ 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 b6a2143..1c013ec 100644 --- a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj +++ b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj @@ -1,31 +1,29 @@  - net48;net8.0 - HttpClient.Caching.Tests - HttpClient.Caching.Tests - false - HttpClient.Caching.Tests - en + net462;net10.0 + enable + latest + enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + - + diff --git a/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs index 6be1772..f7045f6 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs @@ -1,14 +1,8 @@ -using System; -using System.Net.Http; -using FluentAssertions; -using Microsoft.Extensions.Caching.InMemory; -using Xunit; - -namespace HttpClient.Caching.Tests.InMemory +namespace HttpClient.Caching.Tests.InMemory { public class DefaultCacheKeysProviderTests { - private readonly string url = "http://unittest/"; + private const string TestUrl = "http://unittest/"; [Fact] public void ShouldGetKey() @@ -17,7 +11,7 @@ public void ShouldGetKey() var cacheKeysProvider = new DefaultCacheKeysProvider(); var request = new HttpRequestMessage { - RequestUri = new Uri(this.url), + RequestUri = new Uri(TestUrl), Method = HttpMethod.Get, }; diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs index 67b1492..5cbe604 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs @@ -1,125 +1,116 @@ -using System; -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 Moq; -using Xunit; - -namespace HttpClient.Caching.Tests.InMemory +namespace HttpClient.Caching.Tests.InMemory { + using HttpClient = System.Net.Http.HttpClient; + public class InMemoryCacheFallbackHandlerTests { - private readonly string url = "http://unittest/"; + private const string TestUrl = "http://unittest/"; [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 - 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); + // Act twice + await client.GetAsync(TestUrl); + cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl).Should().NotBeNull(); // ensure it's cached before the 2nd call + await client.GetAsync(TestUrl); - // 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)); - - // execute 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); - cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url), Times.Exactly(2)); + cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + + // Act twice, validate cache is called each time + await client.GetAsync(TestUrl); + cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl), Times.Once); + await client.GetAsync(TestUrl); + cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl), Times.Exactly(2)); } [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)); - - // execute 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); - cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url), Times.Once); + cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl)); + cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + TestUrl)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + + // Act twice, validate cache is called each time + await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, TestUrl)); + await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, TestUrl)); + cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + TestUrl), Times.Once); + cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl), Times.Once); } [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)); + cache.Setup(c => c.TryGetValue(TestUrl, out expectedValue)).Returns(false); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute - await client.GetAsync(this.url); + // Act + await client.GetAsync(TestUrl); - // 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; - 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)); + object? expectedValue; + cache.Setup(c => c.TryGetValue(TestUrl, out expectedValue)).Returns(false); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute - var result = await client.GetAsync(this.url); + // Act + var result = await client.GetAsync(TestUrl); - // 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 - var result1 = await client1.GetAsync(this.url); - cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url).Should().NotBeNull(); - var result2 = await client2.GetAsync(this.url); + // Act twice + var result1 = await client1.GetAsync(TestUrl); + cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl).Should().NotBeNull(); + var result2 = await client2.GetAsync(TestUrl); - // validate + // Assert // - that each message handler got called testMessageHandler1.NumberOfCalls.Should().Be(1); testMessageHandler2.NumberOfCalls.Should().Be(1); @@ -134,18 +125,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 - var result1 = await client1.GetAsync(this.url); - var result2 = await client2.GetAsync(this.url); + // Act twice + var result1 = await client1.GetAsync(TestUrl); + var result2 = await client2.GetAsync(TestUrl); - // validate + // Assert // - that each message handler got called testMessageHandler1.NumberOfCalls.Should().Be(1); testMessageHandler2.NumberOfCalls.Should().Be(1); @@ -157,4 +148,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 2c82da3..5829447 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs @@ -1,17 +1,7 @@ -using System; -using System.Collections.Generic; -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 Xunit; - -namespace HttpClient.Caching.Tests.InMemory +namespace HttpClient.Caching.Tests.InMemory { + using HttpClient = System.Net.Http.HttpClient; + public class InMemoryCacheHandlerTests { [Fact] @@ -19,7 +9,7 @@ public async Task CachesTheResult() { // Arrange var testMessageHandler = new TestMessageHandler(); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler)); // Act await client.GetAsync("http://unittest"); @@ -29,13 +19,13 @@ public async Task CachesTheResult() testMessageHandler.NumberOfCalls.Should().Be(1); } -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER [Fact] public void CachesTheResult_Send() { // Arrange var testMessageHandler = new TestMessageHandler(); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler)); // Act var response1 = client.Send(new HttpRequestMessage(HttpMethod.Get, "http://unittest")); @@ -53,7 +43,6 @@ public void CachesTheResult_Send() /// By using without any header then should /// behave like using /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProvider() { @@ -61,7 +50,7 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProvider() var testMessageHandler = new TestMessageHandler(); // no headers are provided so ICacheKeyProvider MethodUriHeadersCacheKeysProvider should behave like DefaultCacheKeysProvider var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(null); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); // Act await client.GetAsync("http://unittest"); @@ -80,8 +69,8 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -97,14 +86,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// both requests specify different value for the header /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_DifferentValues() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -122,14 +110,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// one request specify a value for the header, the other none doesn't specify any hader value /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_HeaderValueInOneRequest() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -146,14 +133,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// both requests specify same value for the header /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -171,14 +157,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// both requests specify same value for the headers /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues_MultipleHeaders() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); request1.Headers.Add("ANOTHER-HEADER", "Value2"); @@ -201,14 +186,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// both requests specify same value for the headers that are common but not all specific request contains the same /// headers /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues_MultipleHeaders2() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); request1.Headers.Add("ANOTHER-HEADER", "Value2"); @@ -229,14 +213,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// both requests specify same value for the headers but in different order /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues_MultipleHeaders_DifferentOrder() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); request1.Headers.Add("HEADER3", "Value3"); @@ -259,14 +242,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// both requests specify same value for specific header. /// A request include some headers which aren't considered for cache key composition /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues2() { // Arrange var testMessageHandler = new TestMessageHandler(); var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); request1.Headers.Add("ANOTHER-CUSTOM-HEADER", "ValueX"); // this header isn't considered for cache key composition @@ -290,7 +272,7 @@ public async Task GetsTheDataAgainAfterEntryIsGoneFromCache() var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); var inMemoryCacheHandler = new InMemoryCacheHandler(testMessageHandler, null, null, cache, null); - var client = new System.Net.Http.HttpClient(inMemoryCacheHandler); + var client = new HttpClient(inMemoryCacheHandler); // Act await client.GetAsync("http://unittest"); @@ -308,7 +290,7 @@ public async Task IsCaseSensitive() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different URLs, only different by casing await client.GetAsync("http://unittest/foo.html"); @@ -324,7 +306,7 @@ public async Task CachesPerUrl() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different URLs await client.GetAsync("http://unittest1"); @@ -341,7 +323,7 @@ public async Task CachesWithCacheControlHeaders_NoCacheTrue() var cacheControl = new CacheControlHeaderValue{ NoCache = true, NoStore = true }; var testMessageHandler = new TestMessageHandler(cacheControl: cacheControl); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -360,7 +342,7 @@ public async Task OnlyCachesGetAndHeadResults() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different methods await client.PostAsync("http://unittest", new StringContent(string.Empty)); @@ -380,7 +362,7 @@ public async Task CachesHeadAndGetRequestWithoutConflict() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different methods await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "http://unittest")); @@ -396,7 +378,7 @@ public async Task DataFromCallMatchesDataFromCache() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different methods var originalResult = await client.GetAsync("http://unittest"); @@ -414,12 +396,14 @@ public async Task ReturnsResponseHeader() { // Arrange var testMessageHandler = new TestMessageHandler(HttpStatusCode.OK, "test content", "text/plain", Encoding.UTF8); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler)); // Act 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"); } @@ -434,7 +418,7 @@ public async Task DisableCachePerStatusCode() }; var testMessageHandler = new TestMessageHandler(); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheExpirationPerStatusCode)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheExpirationPerStatusCode)); // Act await client.GetAsync("http://unittest"); @@ -450,7 +434,7 @@ public async Task InvalidatesCacheCorrectly() // Arrange var testMessageHandler = new TestMessageHandler(); var handler = new InMemoryCacheHandler(testMessageHandler); - var client = new System.Net.Http.HttpClient(handler); + var client = new HttpClient(handler); // Act, with cache invalidation in between var uri = new Uri("http://unittest"); @@ -468,7 +452,7 @@ public async Task InvalidatesCachePerMethod() // Arrange var testMessageHandler = new TestMessageHandler(); var handler = new InMemoryCacheHandler(testMessageHandler); - var client = new System.Net.Http.HttpClient(handler); + var client = new HttpClient(handler); // Act with two methods, and clean up one cache var uri = new Uri("http://unittest"); @@ -487,4 +471,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..a6ee006 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs @@ -1,14 +1,8 @@ -using System; -using System.Net.Http; -using FluentAssertions; -using Microsoft.Extensions.Caching.InMemory; -using Xunit; - -namespace HttpClient.Caching.Tests.InMemory +namespace HttpClient.Caching.Tests.InMemory { public class MethodUriHeadersCacheKeysProviderTests { - private readonly string url = "http://unittest/"; + private const string TestUrl = "http://unittest/"; [Fact] public void ShouldGetKey_EmptyHeaderNames() @@ -18,7 +12,7 @@ public void ShouldGetKey_EmptyHeaderNames() var cacheKeysProvider = new MethodUriHeadersCacheKeysProvider(headersNames); var request = new HttpRequestMessage { - RequestUri = new Uri(this.url), + RequestUri = new Uri(TestUrl), Method = HttpMethod.Get }; request.Headers.Add("X-HEADER-1", "Value1"); @@ -38,7 +32,7 @@ public void ShouldGetKey_WithMatchingHeader() var cacheKeysProvider = new MethodUriHeadersCacheKeysProvider(headersNames); var request = new HttpRequestMessage { - RequestUri = new Uri(this.url), + RequestUri = new Uri(TestUrl), Method = HttpMethod.Get }; request.Headers.Add("X-HEADER-1", "Value1"); @@ -58,7 +52,7 @@ public void ShouldGetKey_WithoutMatchingHeader() var cacheKeysProvider = new MethodUriHeadersCacheKeysProvider(headersNames); var request = new HttpRequestMessage { - RequestUri = new Uri(this.url), + RequestUri = new Uri(TestUrl), Method = HttpMethod.Get }; request.Headers.Add("X-HEADER-3", "Value3"); diff --git a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs deleted file mode 100644 index 4d89f7f..0000000 --- a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using FluentAssertions; -using HttpClient.Caching.Tests.Testdata; -using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.InMemory; -using Xunit; -using Xunit.Abstractions; - -namespace HttpClient.Caching.Tests -{ - public class MemoryCacheTests - { - private readonly ITestOutputHelper testOutputHelper; - - public MemoryCacheTests(ITestOutputHelper testOutputHelper) - { - this.testOutputHelper = testOutputHelper; - } - - [Fact] - public void ShouldSetCache() - { - // Arrange - var expirationTimeSpan = TimeSpan.FromHours(1); - var options = new MemoryCacheOptions(); - var cache = new MemoryCache(options); - var cacheEntryOptions = new MemoryCacheEntryOptions { SlidingExpiration = expirationTimeSpan }; - - // Act - for (var i = 1; i <= 10; i++) - { - cache.Set($"{i}", new TestPayload(i), cacheEntryOptions); - } - - // Assert - cache.TryGetValue("1", out var result1); - result1.Should().NotBeNull(); - result1.Should().BeOfType().Which.Id.Should().Be(1); - 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 6e45993..b9460f2 100644 --- a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs +++ b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs @@ -1,12 +1,4 @@ -using System; -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 +namespace HttpClient.Caching.Tests.TestData { internal class TestMessageHandler : HttpMessageHandler { @@ -17,8 +9,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 +19,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,22 +51,22 @@ 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(); } -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { this.NumberOfCalls++; - if (this.delay != default) + if (this.delay is TimeSpan delay) { - Thread.Sleep(this.delay); + Thread.Sleep(delay); } return this.CreateHttpResponseMessage(); diff --git a/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs b/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs index d9534ce..ce2feb8 100644 --- a/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs +++ b/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs @@ -1,7 +1,4 @@ -using System; -using System.Diagnostics; - -namespace HttpClient.Caching.Tests.Testdata +namespace HttpClient.Caching.Tests.TestData { [DebuggerDisplay("{this.Id}")] public class TestPayload diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7318766..f068a3c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,32 +1,31 @@ #################################################################### -# VSTS Build Configuration, Version 1.4 +# Azure DevOps Build Configuration # -# (c)2022 superdev GmbH +# (c)2026 superdev GmbH #################################################################### name: $[format('{0}', variables['buildName'])] pool: - vmImage: 'windows-2022' + vmImage: 'windows-latest' trigger: branches: include: - - main - - develop - - feature/* - - bugfix/* + - main + - develop + - feature/* + - bugfix/* paths: exclude: - - Docs/* + - Docs/* variables: - solution: 'HttpClient.Caching.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' - majorVersion: 1 - minorVersion: 6 + majorVersion: 2 + minorVersion: 0 patchVersion: $[counter(format('{0}.{1}', variables.majorVersion, variables.minorVersion), 0)] ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}: # Versioning: 1.0.0 @@ -40,96 +39,101 @@ variables: buildName: $[format('{0}', variables.semVersion)] steps: -- task: Bash@3 - displayName: 'Print all variables' - inputs: - targetType: 'inline' - script: 'env | sort' - -- task: Assembly-Info-NetCore@2 - displayName: 'Update Assembly Info' - inputs: - Path: '$(Build.SourcesDirectory)' - FileNames: | - **/*.csproj - InsertAttributes: true - FileEncoding: 'auto' - WriteBOM: false - Product: 'HttpClient.Caching' - Description: '' - Company: 'superdev GmbH' - Copyright: '(c) $(date:YYYY) superdev GmbH' - VersionNumber: '$(Build.BuildNumber)' - FileVersionNumber: '$(Build.BuildNumber)' - InformationalVersion: '$(Build.BuildNumber)' - PackageVersion: '$(Build.BuildNumber)' - LogLevel: 'verbose' - FailOnWarning: false - DisableTelemetry: true - -- task: UseDotNet@2 - displayName: 'Use .NET 8.x' - inputs: - version: 8.x - -- task: NuGetToolInstaller@0 - displayName: 'Use NuGet 6.x' - inputs: - versionSpec: 6.x - -- task: DotNetCoreCLI@2 - displayName: 'NuGet restore' - inputs: - command: restore - projects: '$(solution)' - -- task: DotNetCoreCLI@2 - displayName: 'Build solution' - inputs: - projects: '$(solution)' - arguments: '--no-restore --configuration $(buildConfiguration)' - -- task: DotNetCoreCLI@2 - displayName: 'Run UnitTests' - inputs: - command: test - projects: '**/*.Tests.csproj' - arguments: '--no-restore --no-build --configuration $(buildConfiguration) --framework net8.0 /p:CollectCoverage=true /p:Exclude="[Microsoft*]*%2C[Mono*]*%2C[xunit*]*%2C[*.Testdata]*" /p:CoverletOutput=UnitTests.coverage.cobertura.xml /p:MergeWith=$(Build.SourcesDirectory)/Tests/CoverletOutput/coverage.json /p:CoverletOutputFormat=cobertura' - -- task: reportgenerator@5 - displayName: 'Create Code Coverage Report' - inputs: - reports: '$(Build.SourcesDirectory)/Tests/**/*.coverage.cobertura*.xml' - targetdir: '$(Build.SourcesDirectory)/CodeCoverage' - reporttypes: 'Cobertura' - assemblyfilters: '-xunit*' - -- task: DotNetCoreCLI@2 - displayName: 'Pack HttpClient.Caching' - inputs: - command: pack - packagesToPack: HttpClient.Caching/HttpClient.Caching.csproj - versioningScheme: byEnvVar - versionEnvVar: semVersion - nobuild: true - -- task: CopyFiles@2 - displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)' - inputs: - SourceFolder: '$(system.defaultworkingdirectory)' - - Contents: | - **\bin\$(BuildConfiguration)\** - **\bin\*.nupkg - - TargetFolder: '$(Build.ArtifactStagingDirectory)' - -- task: PublishCodeCoverageResults@2 - displayName: 'Publish code coverage' - inputs: - codeCoverageTool: Cobertura - summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml' - reportDirectory: '$(Build.SourcesDirectory)/CodeCoverage' - -- task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: drop' + - task: Bash@3 + displayName: 'Print all variables' + inputs: + targetType: 'inline' + script: 'env | sort' + + - task: Assembly-Info-NetCore@3 + displayName: 'Update Assembly Info' + inputs: + Path: '$(Build.SourcesDirectory)' + FileNames: | + **/*.csproj + InsertAttributes: true + FileEncoding: 'auto' + WriteBOM: false + Product: 'HttpClient.Caching' + Description: '' + Company: 'superdev GmbH' + Copyright: '(c) $(date:YYYY) superdev GmbH' + VersionNumber: '$(Build.BuildNumber)' + FileVersionNumber: '$(Build.BuildNumber)' + InformationalVersion: '$(Build.BuildNumber)' + PackageVersion: '$(Build.BuildNumber)' + LogLevel: 'verbose' + FailOnWarning: false + DisableTelemetry: true + + - task: UseDotNet@2 + displayName: 'Use .NET SDK from global.json' + inputs: + packageType: 'sdk' + useGlobalJson: true + workingDirectory: '$(Build.SourcesDirectory)' + + - task: NuGetToolInstaller@0 + displayName: 'Use NuGet 6.x' + inputs: + versionSpec: 6.x + + - task: DotNetCoreCLI@2 + displayName: 'NuGet restore' + inputs: + command: restore + projects: | + HttpClient.Caching/**/*.csproj + **/*.Tests.csproj + + - task: DotNetCoreCLI@2 + displayName: 'Build solution' + inputs: + projects: | + HttpClient.Caching/**/*.csproj + **/*.Tests.csproj + arguments: '--no-restore --configuration $(buildConfiguration)' + + - task: DotNetCoreCLI@2 + displayName: 'Run UnitTests' + inputs: + command: test + projects: '**/*.Tests.csproj' + arguments: '--no-restore --no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:Exclude="[Microsoft*]*%2C[Mono*]*%2C[xunit*]*%2C[*.Testdata]*" /p:CoverletOutput=UnitTests.coverage.cobertura.xml /p:MergeWith=$(Build.SourcesDirectory)/Tests/CoverletOutput/coverage.json /p:CoverletOutputFormat=cobertura' + + - task: reportgenerator@5 + displayName: 'Create Code Coverage Report' + inputs: + reports: '$(Build.SourcesDirectory)/Tests/**/*.coverage.cobertura*.xml' + targetdir: '$(Build.SourcesDirectory)/CodeCoverage' + reporttypes: 'Cobertura' + assemblyfilters: '-xunit*' + + - task: DotNetCoreCLI@2 + displayName: 'Pack HttpClient.Caching' + inputs: + command: pack + packagesToPack: HttpClient.Caching/HttpClient.Caching.csproj + versioningScheme: byEnvVar + versionEnvVar: semVersion + nobuild: true + + - task: CopyFiles@2 + displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)' + inputs: + SourceFolder: '$(system.defaultworkingdirectory)' + Contents: | + **\bin\*.nupkg + **\bin\*.snupkg + **\ReleaseNotes.txt + TargetFolder: '$(Build.ArtifactStagingDirectory)' + + - task: PublishCodeCoverageResults@2 + displayName: 'Publish code coverage' + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml' + reportDirectory: '$(Build.SourcesDirectory)/CodeCoverage' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: drop' diff --git a/global.json b/global.json new file mode 100644 index 0000000..31d78e1 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.201", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/Images/logo.png b/logo.png similarity index 100% rename from Images/logo.png rename to logo.png