diff --git a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs index 42aa3b9..9738768 100644 --- a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs +++ b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs @@ -177,6 +177,137 @@ public void AddWithSlidingThatExpiresReturnsNull() Assert.IsNull(sut.Get(TestKey)); } + [Test] + public void SetComplexObjectThenGetGenericReturnsCachedObject() + { + testObject.SomeItems.Add("Another"); + testObject.SomeMessage = "changed-it-up"; + sut.Set(TestKey, testObject); + var actual = sut.Get(TestKey); + var expected = testObject; + Assert.NotNull(actual); + Assert.AreEqual(expected, actual); + testObject.SomeItems.Should().Contain("Another"); + testObject.SomeMessage.Should().Be("changed-it-up"); + } + + [Test] + public void SetComplexObjectThenGetReturnsCachedObject() + { + sut.Set(TestKey, testObject); + var actual = sut.Get(TestKey) as ComplexTestObject; + var expected = testObject; + Assert.NotNull(actual); + Assert.AreEqual(expected, actual); + } + + [Test] + public void SetEmptyKeyThrowsException() + { + Action act = () => sut.Set("", new object()); + act.Should().Throw(); + } + + [Test] + public void SetEmptyKeyThrowsExceptionWithExpiration() + { + Action act = () => sut.Set("", new object(), DateTimeOffset.Now.AddHours(1)); + act.Should().Throw(); + } + + [Test] + public void SetEmptyKeyThrowsExceptionWithPolicy() + { + Action act = () => sut.Set("", new object(), new MemoryCacheEntryOptions()); + act.Should().Throw(); + } + + [Test] + public void SetEmptyKeyThrowsExceptionWithSliding() + { + Action act = () => sut.Set("", new object(), new TimeSpan(1000)); + act.Should().Throw(); + } + + [Test] + public void SetNullKeyThrowsException() + { + Action act = () => sut.Set(null, new object()); + act.Should().Throw(); + } + + [Test] + public void SetNullKeyThrowsExceptionWithExpiration() + { + Action act = () => sut.Set(null, new object(), DateTimeOffset.Now.AddHours(1)); + act.Should().Throw(); + } + + [Test] + public void SetNullKeyThrowsExceptionWithPolicy() + { + Action act = () => sut.Set(null, new object(), new MemoryCacheEntryOptions()); + act.Should().Throw(); + } + + [Test] + public void SetNullKeyThrowsExceptionWithSliding() + { + Action act = () => sut.Set(null, new object(), new TimeSpan(1000)); + act.Should().Throw(); + } + + [Test] + public void SetNullThrowsException() + { + Action act = () => sut.Add(TestKey, null); + act.Should().Throw(); + } + + [Test] + public void SetThenGetReturnsCachedObject() + { + sut.Set(TestKey, "testObject"); + Assert.AreEqual("testObject", sut.Get(TestKey)); + } + + [Test] + public void SetWithOffsetReturnsCachedItem() + { + sut.Set(TestKey, "testObject", DateTimeOffset.Now.AddSeconds(1)); + Assert.AreEqual("testObject", sut.Get(TestKey)); + } + + [Test] + public void SetWithOffsetThatExpiresReturnsNull() + { + sut.Set(TestKey, "testObject", DateTimeOffset.Now.AddSeconds(1)); + Thread.Sleep(1500); + Assert.IsNull(sut.Get(TestKey)); + } + + [Test] + public void SetWithPolicyReturnsCachedItem() + { + sut.Set(TestKey, "testObject", new MemoryCacheEntryOptions()); + Assert.AreEqual("testObject", sut.Get(TestKey)); + } + + [Test] + public void SetWithSlidingReturnsCachedItem() + { + sut.Set(TestKey, "testObject", new TimeSpan(5000)); + Assert.AreEqual("testObject", sut.Get(TestKey)); + } + + [Test] + public void SetWithSlidingThatExpiresReturnsNull() + { + sut.Set(TestKey, "testObject", new TimeSpan(750)); + Thread.Sleep(1500); + Assert.IsNull(sut.Get(TestKey)); + } + [Test] public void CacheProviderIsNotNull() { diff --git a/LazyCache.UnitTests/DeprecationTests.cs b/LazyCache.UnitTests/DeprecationTests.cs new file mode 100644 index 0000000..2bd9028 --- /dev/null +++ b/LazyCache.UnitTests/DeprecationTests.cs @@ -0,0 +1,106 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace LazyCache.UnitTests +{ + /// + /// Tests to confirm that methods intended to be deprecated have been adequately marked as such + /// + public class DeprecationTests + { + [Test] + public void AllImplementationsOfIAppCache_HaveTheAddMethod_MarkedAsObsolete() + { + var classesWithoutObsoleteTag = new List(); + + foreach (var type in GetTypesWithIAppCache()) + { + if (MethodDoesNotHaveObsoleteAttribute(type)) + { + classesWithoutObsoleteTag.Add(type); + } + } + CollectionAssert.IsEmpty(classesWithoutObsoleteTag, "Types which do not have the Add() method marked as Obsolete"); + } + + [Test] + public void AllImplementationsOfIAppCache_HaveTheAddMethod_MarkedAsNeverBrowsable() + { + var classesWhereAddAppearsBrowsable = new List(); + + foreach (var type in GetTypesWithIAppCache()) + { + var editorBrowsableState = GetAddMethodsEditorBrowsableState(type); + if (editorBrowsableState != EditorBrowsableState.Never) + { + classesWhereAddAppearsBrowsable.Add(type); + } + } + CollectionAssert.IsEmpty(classesWhereAddAppearsBrowsable, "Types which have the Add() method not marked as Never browsable"); + } + + [Test] + public void AppCacheExtensions_AddMethods_AreObsolete() + { + var addMethodsWithoutObsoleteAttribute = + typeof(AppCacheExtensions).GetMethods() + .Where(m => m.Name == nameof(AppCacheExtensions.Add) + && m.GetCustomAttribute(typeof(ObsoleteAttribute), true) == null); + + CollectionAssert.IsEmpty(addMethodsWithoutObsoleteAttribute, "Add methods not marked as obsolete"); + } + + [Test] + public void AppCacheExtensions_AddMethods_AreNotBrowsable() + { + var addMethodsThatAreBrowsable = + typeof(AppCacheExtensions) + .GetMethods() + .Where(m => m.Name == nameof(AppCacheExtensions.Add)) + .Select(m => (Method: m, Attribute: GetEditorBrowsableAttribute(m))) + .Where(ma => ma.Attribute.State != EditorBrowsableState.Never); + + CollectionAssert.IsEmpty(addMethodsThatAreBrowsable.Select(ma => ma.Method), "Add methods not marked as Never browsable"); + + static EditorBrowsableAttribute GetEditorBrowsableAttribute(MethodInfo m) + { + return m.GetCustomAttributes(typeof(EditorBrowsableAttribute), true).Single() as EditorBrowsableAttribute; + } + } + + private IEnumerable GetTypesWithIAppCache() + { + var iAppCache = typeof(IAppCache); + return Assembly.GetAssembly(iAppCache) + .GetTypes() + .Where(iAppCache.IsAssignableFrom); + } + + private bool MethodDoesNotHaveObsoleteAttribute(Type type) + { + var method = type.GetMethods() + .SingleOrDefault(m => m.Name == nameof(IAppCache.Add)); + var attribute = method?.GetCustomAttributes(typeof(ObsoleteAttribute), true) + .SingleOrDefault() as ObsoleteAttribute; + return attribute == null; + } + + private EditorBrowsableState GetAddMethodsEditorBrowsableState(Type type) + { + var addMethod = nameof(IAppCache.Add); + var method = type.GetMethods() + .SingleOrDefault(m => m.Name == addMethod); + var attribute = method?.GetCustomAttributes(typeof(EditorBrowsableAttribute), true) + .SingleOrDefault() as EditorBrowsableAttribute; + if (attribute == null) + { + throw new InvalidOperationException($"{type.Name}'s {addMethod} method does not have {nameof(EditorBrowsableAttribute)}"); + } + return attribute.State; + } + } +} \ No newline at end of file diff --git a/LazyCache/AppCacheExtensions.cs b/LazyCache/AppCacheExtensions.cs index 9d4a823..bf28b81 100644 --- a/LazyCache/AppCacheExtensions.cs +++ b/LazyCache/AppCacheExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; @@ -8,25 +9,25 @@ namespace LazyCache { public static class AppCacheExtensions { + [Obsolete("This method has been deprecated. Use Set instead.", false)] + [EditorBrowsable(EditorBrowsableState.Never)] public static void Add(this IAppCache cache, string key, T item) { - if (cache == null) throw new ArgumentNullException(nameof(cache)); - - cache.Add(key, item, cache.DefaultCachePolicy.BuildOptions()); + Set(cache, key, item); } + [Obsolete("This method has been deprecated. Use Set instead.", false)] + [EditorBrowsable(EditorBrowsableState.Never)] public static void Add(this IAppCache cache, string key, T item, DateTimeOffset expires) { - if (cache == null) throw new ArgumentNullException(nameof(cache)); - - cache.Add(key, item, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); + Set(cache, key, item, expires); } + [Obsolete("This method has been deprecated. Use Set instead.", false)] + [EditorBrowsable(EditorBrowsableState.Never)] public static void Add(this IAppCache cache, string key, T item, TimeSpan slidingExpiration) { - if (cache == null) throw new ArgumentNullException(nameof(cache)); - - cache.Add(key, item, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); + Set(cache, key, item, slidingExpiration); } public static T GetOrAdd(this IAppCache cache, string key, Func addItemFactory) @@ -116,5 +117,26 @@ public static Task GetOrAddAsync(this IAppCache cache, string key, Func addItemFactory(), policy); } + + public static void Set(this IAppCache cache, string key, T item) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + cache.Set(key, item, cache.DefaultCachePolicy.BuildOptions()); + } + + public static void Set(this IAppCache cache, string key, T item, DateTimeOffset expires) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + cache.Set(key, item, new MemoryCacheEntryOptions { AbsoluteExpiration = expires }); + } + + public static void Set(this IAppCache cache, string key, T item, TimeSpan slidingExpiration) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + cache.Set(key, item, new MemoryCacheEntryOptions { SlidingExpiration = slidingExpiration }); + } } } \ No newline at end of file diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 389ddcd..581ae09 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using LazyCache.Providers; @@ -62,13 +63,11 @@ public virtual int DefaultCacheDuration /// public virtual CacheDefaults DefaultCachePolicy { get; set; } = new CacheDefaults(); + [Obsolete("This method has been deprecated. Use Set instead.", false)] + [EditorBrowsable(EditorBrowsableState.Never)] public virtual void Add(string key, T item, MemoryCacheEntryOptions policy) { - if (item == null) - throw new ArgumentNullException(nameof(item)); - ValidateKey(key); - - CacheProvider.Set(key, item, policy); + Set(key, item, policy); } public virtual T Get(string key) @@ -155,15 +154,6 @@ object CacheFactory(ICacheEntry entry) => } } - private static void SetAbsoluteExpirationFromRelative(ICacheEntry entry) - { - if (!entry.AbsoluteExpirationRelativeToNow.HasValue) return; - - var absoluteExpiration = DateTimeOffset.UtcNow + entry.AbsoluteExpirationRelativeToNow.Value; - if (!entry.AbsoluteExpiration.HasValue || absoluteExpiration < entry.AbsoluteExpiration) - entry.AbsoluteExpiration = absoluteExpiration; - } - public virtual void Remove(string key) { ValidateKey(key); @@ -172,10 +162,11 @@ public virtual void Remove(string key) public virtual ICacheProvider CacheProvider => cacheProvider.Value; - public virtual async Task GetOrAddAsync(string key, Func> addItemFactory) + public virtual Task GetOrAddAsync(string key, Func> addItemFactory) { - return await GetOrAddAsync(key, addItemFactory, null); + return GetOrAddAsync(key, addItemFactory, null); } + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy) { @@ -247,6 +238,15 @@ object CacheFactory(ICacheEntry entry) => } } + public virtual void Set(string key, T item, MemoryCacheEntryOptions policy) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + ValidateKey(key); + + CacheProvider.Set(key, item, policy); + } + protected virtual T GetValueFromLazy(object item, out bool valueHasChangedType) { valueHasChangedType = false; @@ -334,5 +334,14 @@ protected virtual void ValidateKey(string key) if (string.IsNullOrWhiteSpace(key)) throw new ArgumentOutOfRangeException(nameof(key), "Cache keys cannot be empty or whitespace"); } + + private static void SetAbsoluteExpirationFromRelative(ICacheEntry entry) + { + if (!entry.AbsoluteExpirationRelativeToNow.HasValue) return; + + var absoluteExpiration = DateTimeOffset.UtcNow + entry.AbsoluteExpirationRelativeToNow.Value; + if (!entry.AbsoluteExpiration.HasValue || absoluteExpiration < entry.AbsoluteExpiration) + entry.AbsoluteExpiration = absoluteExpiration; + } } } \ No newline at end of file diff --git a/LazyCache/IAppCache.cs b/LazyCache/IAppCache.cs index b171411..caad282 100644 --- a/LazyCache/IAppCache.cs +++ b/LazyCache/IAppCache.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; @@ -12,6 +13,8 @@ public interface IAppCache /// Define the number of seconds to cache objects for by default /// CacheDefaults DefaultCachePolicy { get; } + [Obsolete("This method has been deprecated. Use Set instead.", false)] + [EditorBrowsable(EditorBrowsableState.Never)] void Add(string key, T item, MemoryCacheEntryOptions policy); T Get(string key); Task GetAsync(string key); @@ -20,5 +23,6 @@ public interface IAppCache Task GetOrAddAsync(string key, Func> addItemFactory); Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy); void Remove(string key); + void Set(string key, T item, MemoryCacheEntryOptions policy); } } \ No newline at end of file diff --git a/LazyCache/Mocks/MockCachingService.cs b/LazyCache/Mocks/MockCachingService.cs index 71016c9..5c8210f 100644 --- a/LazyCache/Mocks/MockCachingService.cs +++ b/LazyCache/Mocks/MockCachingService.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; @@ -48,7 +49,14 @@ public Task GetAsync(string key) return Task.FromResult(default(T)); } + [Obsolete("This method has been deprecated. Use Set instead.", false)] + [EditorBrowsable(EditorBrowsableState.Never)] public void Add(string key, T item, MemoryCacheEntryOptions policy) + { + Set(key, item, policy); + } + + public void Set(string key, T item, MemoryCacheEntryOptions policy) { } } diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 53150ae..b2df8e6 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -3,6 +3,7 @@ ## Version 2.1.3 - Rename ExpirationMode.ImmediateExpiry => ExpirationMode.ImmediateEviction - Lovely new logo! (#133) Thanks to @doolali +- Performance imporvements to reduce allocations in #134 - thanks @jnyrup ## Version 2.1.2 - Tweak key lock array size based on CPU count so larger for bigger machines (See PR #126 and discussion with @jjxtra)