diff --git a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs index f381820..4e8710a 100644 --- a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs +++ b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs @@ -127,10 +127,32 @@ public void AddNullKeyThrowsExceptionWithSliding() } [Test] - public void AddNullThrowsException() + public void AddNullDoesNotThrowException() { Action act = () => sut.Add(TestKey, null); - act.Should().Throw(); + act.Should().NotThrow(); + } + + [Test] + public void AddNullThenGetReturnsCachedNullReference() + { + const string testValue = null; + + sut.Add(TestKey, testValue); + + Assert.IsNull(sut.Get(TestKey)); + } + + [Test] + public void AddNullThenTryGetReturnsTrueAndTheCachedNullReference() + { + const string testValue = null; + sut.Add(TestKey, testValue); + + var contains = sut.TryGetValue(TestKey, out var value); + + Assert.IsTrue(contains); + Assert.IsNull(value); } [Test] @@ -1109,11 +1131,42 @@ public void RemovedItemCannotBeRetrievedFromCache() Assert.Null(sut.Get(TestKey)); } + + [Test] + public void TryGetValueThrowsWhenKeyIsNull() + { + TestDelegate testDelegate = () => sut.TryGetValue(null, out var _); + + var exception = Assert.Throws(testDelegate); + + Assert.AreEqual("key", exception.ParamName); + } + + [Test] + public void TryGetValueThrowsWhenKeyIsEmptyString() + { + TestDelegate testDelegate = () => sut.TryGetValue(string.Empty, out var _); + + var exception = Assert.Throws(testDelegate); + + Assert.AreEqual("key", exception.ParamName); + } + + [Test] + public void TryGetValueThrowsWhenKeyIsWhiteSpacesString() + { + TestDelegate testDelegate = () => sut.TryGetValue(" ", out var _); + + var exception = Assert.Throws(testDelegate); + + Assert.AreEqual("key", exception.ParamName); + } + [Test] - public void TryGetReturnsCachedValueAndTrue() + public void TryGetValueReturnsCachedValueAndTrue() { - string val = "Test Value"; - string key = "testkey"; + const string val = "Test Value"; + const string key = "testkey"; sut.Add(key, val); var contains = sut.TryGetValue(key, out var value); @@ -1124,6 +1177,421 @@ public void TryGetReturnsCachedValueAndTrue() var contains2 = sut.TryGetValue("invalidkey", out var value2); Assert.IsFalse(contains2); + Assert.IsNull(value2); + } + + [Test] + public void TryGetValueReturnsCachedComplexObjectAndTrue() + { + sut.Add(TestKey, testObject); + + var contains = sut.TryGetValue(TestKey, out var value); + + Assert.IsTrue(contains); + Assert.AreEqual(testObject, value); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.IsNull(value2); + } + + [Test] + public void TryGetValueReturnsCachedValueTypeAndTrue() + { + const int value = 13; + const string key = "testkey"; + sut.Add(key, value); + + var contains = sut.TryGetValue(key, out var fetchedValue); + + Assert.IsTrue(contains); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.AreEqual(default(int), value2); + } + + [Test] + public void TryGetValueReturnsCachedStructAndTrue() + { + var value = new DateTime(2021, 6, 20, 10, 41, 13); + const string key = "testkey"; + sut.Add(key, value); + + var contains = sut.TryGetValue(key, out var fetchedValue); + + Assert.IsTrue(contains); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.AreEqual(default(DateTime), value2); + } + + [Test] + public void TryGetValueReturnsCachedNullableStructWithValueAndTrue() + { + DateTime? value = new DateTime(2021, 6, 20, 10, 41, 13); + const string key = "testkey"; + sut.Add(key, value); + + var contains = sut.TryGetValue(key, out var fetchedValue); + + Assert.IsTrue(contains); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.IsNull(value2); + } + + [Test] + public void TryGetValueReturnsCachedNullableStructWithoutValueAndTrue() + { + DateTime? value = null; + const string key = "testkey"; + sut.Add(key, value); + + var contains = sut.TryGetValue(key, out var fetchedValue); + + Assert.IsTrue(contains); + Assert.IsNull(fetchedValue); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.IsNull(value2); + } + + [Test] + public void GetOrAddThenTryGetValueReturnsCachedValueAndTrue() + { + const string value = "Test Value"; + const string key = "testkey"; + sut.GetOrAdd(key, () => value); + + var contains = sut.TryGetValue(key, out var fetchedValue); + + Assert.IsTrue(contains); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.IsNull(value2); + } + + [Test] + public void GetOrAddComplexObjectThenTryGetValueReturnsCachedValueAndTrue() + { + sut.GetOrAdd(TestKey, () => testObject); + + var contains = sut.TryGetValue(TestKey, out var value); + + Assert.IsTrue(contains); + Assert.AreEqual(testObject, value); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.IsNull(value2); + } + + [Test] + public void GetOrAddValueTypeThenTryGetValueReturnsCachedValueAndTrue() + { + const int value = 13; + const string key = "testkey"; + sut.GetOrAdd(key, () => value); + + var contains = sut.TryGetValue(key, out var fetchedValue); + + Assert.IsTrue(contains); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.AreEqual(default(int), value2); + } + + [Test] + public void GetOrAddStructThenTryGetValueReturnsCachedValueAndTrue() + { + var value = new DateTime(2021, 6, 20, 10, 41, 13); + const string key = "testkey"; + sut.GetOrAdd(key, () => value); + + var contains = sut.TryGetValue(key, out var fetchedValue); + + Assert.IsTrue(contains); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.AreEqual(default(DateTime), value2); + } + + [Test] + public void GetOrAddNullableStructWithValueThenTryGetValueReturnsCachedValueAndTrue() + { + DateTime? value = new DateTime(2021, 6, 20, 10, 41, 13); + const string key = "testkey"; + sut.GetOrAdd(key, () => value); + + var contains = sut.TryGetValue(key, out var fetchedValue); + + Assert.IsTrue(contains); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.IsNull(value2); + } + + [Test] + public void GetOrAddNullableStructWithoutValueThenTryGetValueReturnsCachedValueAndTrue() + { + DateTime? value = null; + const string key = "testkey"; + sut.GetOrAdd(key, () => value); + + var contains = sut.TryGetValue(key, out var fetchedValue); + + Assert.IsTrue(contains); + Assert.IsNull(fetchedValue); + + var contains2 = sut.TryGetValue("invalidkey", out var value2); + + Assert.IsFalse(contains2); + Assert.IsNull(value2); + } + + + [Test] + public void TryGetValueAsyncThrowsWhenKeyIsNull() + { + TestDelegate testDelegate = () => sut.TryGetValueAsync(null, out var _); + + var exception = Assert.Throws(testDelegate); + + Assert.AreEqual("key", exception.ParamName); + } + + [Test] + public void TryGetValueAsyncThrowsWhenKeyIsEmptyString() + { + TestDelegate testDelegate = () => sut.TryGetValueAsync(string.Empty, out var _); + + var exception = Assert.Throws(testDelegate); + + Assert.AreEqual("key", exception.ParamName); + } + + [Test] + public void TryGetValueAsyncThrowsWhenKeyIsWhiteSpacesString() + { + TestDelegate testDelegate = () => sut.TryGetValueAsync(" ", out var _); + + var exception = Assert.Throws(testDelegate); + + Assert.AreEqual("key", exception.ParamName); + } + + [Test] + public async Task GetOrAddAsyncThenTryGetValueAsyncReturnsCachedValueAndTrue() + { + const string value = "Test Value"; + const string key = "testkey"; + + _ = await sut.GetOrAddAsync( + key, + async () => + { + await Task.Delay(4).ConfigureAwait(false); + return value; + } + ).ConfigureAwait(false); + + var contains = sut.TryGetValueAsync(key, out var taskForValue); + + Assert.IsTrue(contains); + Assert.IsNotNull(taskForValue); + + var fetchedValue = await taskForValue.ConfigureAwait(false); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValueAsync("invalidkey", out var taskForValue2); + + Assert.IsFalse(contains2); + Assert.IsNotNull(taskForValue2); + + var fetchedValue2 = await taskForValue2.ConfigureAwait(false); + Assert.IsNull(fetchedValue2); + } + + [Test] + public async Task GetOrAddAsyncComplexObjectThenTryGetValueAsyncReturnsCachedValueAndTrue() + { + _ = await sut.GetOrAddAsync( + TestKey, + async () => + { + await Task.Delay(4).ConfigureAwait(false); + return testObject; + } + ).ConfigureAwait(false); + + var contains = sut.TryGetValueAsync(TestKey, out var taskForValue); + + Assert.IsTrue(contains); + Assert.IsNotNull(taskForValue); + + var fetchedValue = await taskForValue.ConfigureAwait(false); + Assert.AreEqual(testObject, fetchedValue); + + var contains2 = sut.TryGetValueAsync("invalidkey", out var taskForValue2); + + Assert.IsFalse(contains2); + Assert.IsNotNull(taskForValue2); + + var fetchedValue2 = await taskForValue2.ConfigureAwait(false); + Assert.IsNull(fetchedValue2); + } + + [Test] + public async Task GetOrAddAsyncValueTypeThenTryGetValueAsyncReturnsCachedValueAndTrue() + { + const int value = 13; + const string key = "testkey"; + + _ = await sut.GetOrAddAsync( + key, + async () => + { + await Task.Delay(4).ConfigureAwait(false); + return value; + } + ).ConfigureAwait(false); + + var contains = sut.TryGetValueAsync(key, out var taskForValue); + + Assert.IsTrue(contains); + Assert.IsNotNull(taskForValue); + + var fetchedValue = await taskForValue.ConfigureAwait(false); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValueAsync("invalidkey", out var taskForValue2); + + Assert.IsFalse(contains2); + Assert.IsNotNull(taskForValue2); + + var fetchedValue2 = await taskForValue2.ConfigureAwait(false); + Assert.AreEqual(default(int), fetchedValue2); + } + + [Test] + public async Task GetOrAddAsyncStructThenTryGetValueAsyncReturnsCachedValueAndTrue() + { + var value = new DateTime(2021, 6, 20, 10, 41, 13); + const string key = "testkey"; + + _ = await sut.GetOrAddAsync( + key, + async () => + { + await Task.Delay(4).ConfigureAwait(false); + return value; + } + ).ConfigureAwait(false); + + var contains = sut.TryGetValueAsync(key, out var taskForValue); + + Assert.IsTrue(contains); + Assert.IsNotNull(taskForValue); + + var fetchedValue = await taskForValue.ConfigureAwait(false); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValueAsync("invalidkey", out var taskForValue2); + + Assert.IsFalse(contains2); + Assert.IsNotNull(taskForValue2); + + var fetchedValue2 = await taskForValue2.ConfigureAwait(false); + Assert.AreEqual(default(DateTime), fetchedValue2); + } + + [Test] + public async Task GetOrAddAsyncNullableStructWithValueThenTryGetValueAsyncReturnsCachedValueAndTrue() + { + DateTime? value = new DateTime(2021, 6, 20, 10, 41, 13); + const string key = "testkey"; + + _ = await sut.GetOrAddAsync( + key, + async () => + { + await Task.Delay(4).ConfigureAwait(false); + return value; + } + ).ConfigureAwait(false); + + var contains = sut.TryGetValueAsync(key, out var taskForValue); + + Assert.IsTrue(contains); + Assert.IsNotNull(taskForValue); + + var fetchedValue = await taskForValue.ConfigureAwait(false); + Assert.AreEqual(value, fetchedValue); + + var contains2 = sut.TryGetValueAsync("invalidkey", out var taskForValue2); + + Assert.IsFalse(contains2); + Assert.IsNotNull(taskForValue2); + + var fetchedValue2 = await taskForValue2.ConfigureAwait(false); + Assert.IsNull(fetchedValue2); + } + + [Test] + public async Task GetOrAddAsyncNullableStructWithoutValueThenTryGetValueAsyncReturnsCachedValueAndTrue() + { + DateTime? value = null; + const string key = "testkey"; + + _ = await sut.GetOrAddAsync( + key, + async () => + { + await Task.Delay(4).ConfigureAwait(false); + return value; + } + ).ConfigureAwait(false); + + var contains = sut.TryGetValueAsync(key, out var taskForValue); + + Assert.IsTrue(contains); + Assert.IsNotNull(taskForValue); + + var fetchedValue = await taskForValue.ConfigureAwait(false); + Assert.IsNull(fetchedValue); + + var contains2 = sut.TryGetValueAsync("invalidkey", out var taskForValue2); + + Assert.IsFalse(contains2); + Assert.IsNotNull(taskForValue2); + + var fetchedValue2 = await taskForValue2.ConfigureAwait(false); + Assert.IsNull(fetchedValue2); } } } \ No newline at end of file diff --git a/LazyCache/AppCacheExtensions.cs b/LazyCache/AppCacheExtensions.cs index 9d4a823..d49699a 100644 --- a/LazyCache/AppCacheExtensions.cs +++ b/LazyCache/AppCacheExtensions.cs @@ -1,8 +1,6 @@ using System; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Primitives; namespace LazyCache { diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 6becdd3..4a2e6a8 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using LazyCache.Providers; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Primitives; +using System.Diagnostics.CodeAnalysis; namespace LazyCache { @@ -64,8 +64,6 @@ public virtual int DefaultCacheDuration 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); @@ -89,11 +87,25 @@ public virtual Task GetAsync(string key) return GetValueFromAsyncLazy(item, out _); } - public virtual bool TryGetValue(string key, out object value) + public virtual bool TryGetValue(string key, out T value) { ValidateKey(key); - return CacheProvider.TryGetValue(key, out value); + var keyWasFound = CacheProvider.TryGetValue(key, out var cachedValue); + value = GetValueFromLazy(cachedValue, out _); + + return keyWasFound; + } + + [SuppressMessage("Naming", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "Using Async suffix here it's fine. This method name follows the same pattern used for the other methods in this class.")] + public virtual bool TryGetValueAsync(string key, out Task value) + { + ValidateKey(key); + + var keyWasFound = CacheProvider.TryGetValue(key, out var cachedValue); + value = GetValueFromAsyncLazy(cachedValue, out _); + + return keyWasFound; } public virtual T GetOrAdd(string key, Func addItemFactory) diff --git a/LazyCache/IAppCache.cs b/LazyCache/IAppCache.cs index 3d9133d..68112cb 100644 --- a/LazyCache/IAppCache.cs +++ b/LazyCache/IAppCache.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; +using System.Diagnostics.CodeAnalysis; namespace LazyCache { @@ -15,7 +16,10 @@ public interface IAppCache void Add(string key, T item, MemoryCacheEntryOptions policy); T Get(string key); Task GetAsync(string key); - bool TryGetValue(string key, out object value); + bool TryGetValue(string key, out T value); + + [SuppressMessage("Naming", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "Using Async suffix here it's fine. This method name follows the same pattern used for the other methods in this interface.")] + bool TryGetValueAsync(string key, out Task value); T GetOrAdd(string key, Func addItemFactory); T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy); diff --git a/LazyCache/Mocks/MockCachingService.cs b/LazyCache/Mocks/MockCachingService.cs index 9c9be86..8857eac 100644 --- a/LazyCache/Mocks/MockCachingService.cs +++ b/LazyCache/Mocks/MockCachingService.cs @@ -52,10 +52,16 @@ public void Add(string key, T item, MemoryCacheEntryOptions policy) { } - public bool TryGetValue(string key, out object value) + public bool TryGetValue(string key, out T value) { value = default(T); return true; } + + public bool TryGetValueAsync(string key, out Task value) + { + value = Task.FromResult(default(T)); + return true; + } } } \ No newline at end of file