From ed8d680d0859329455dcfa9ffcfa5ba9dadef1ea Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Fri, 23 Feb 2018 00:31:53 +0000 Subject: [PATCH 01/39] feat: minimal working netstandard 2 version (STILL WIP!) --- LazyCache.UnitTests/CachingServiceTests.cs | 160 +++++++++--------- .../LazyCache.UnitTests.csproj | 101 +++-------- .../Properties/AssemblyInfo.cs | 37 ---- LazyCache.UnitTests/packages.config | 5 - LazyCache/CachingService.cs | 125 +++++++------- LazyCache/IAppCache.cs | 10 +- LazyCache/LazyCache.csproj | 78 ++------- LazyCache/Mocks/MockCachingService.cs | 10 +- LazyCache/Properties/AssemblyInfo.cs | 36 ---- ReleaseNotes.md | 6 + 10 files changed, 191 insertions(+), 377 deletions(-) delete mode 100644 LazyCache.UnitTests/Properties/AssemblyInfo.cs delete mode 100644 LazyCache.UnitTests/packages.config delete mode 100644 LazyCache/Properties/AssemblyInfo.cs diff --git a/LazyCache.UnitTests/CachingServiceTests.cs b/LazyCache.UnitTests/CachingServiceTests.cs index d20ea1a..c7fad3f 100644 --- a/LazyCache.UnitTests/CachingServiceTests.cs +++ b/LazyCache.UnitTests/CachingServiceTests.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.Runtime.Caching; using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using Microsoft.Extensions.Caching.Memory; using NUnit.Framework; namespace LazyCache.UnitTests @@ -14,7 +14,7 @@ public class CachingServiceTests [TearDown] public void TearDown() { - MemoryCache.Default.Remove(TestKey); + //MemoryCache.Default.Remove(TestKey); } [SetUp] @@ -26,10 +26,10 @@ public void BeforeEachTest() private CachingService sut; - private readonly CacheItemPolicy oneHourNonRemoveableCacheItemPolicy = new CacheItemPolicy + private readonly MemoryCacheEntryOptions oneHourNonRemoveableMemoryCacheEntryOptions = new MemoryCacheEntryOptions { AbsoluteExpiration = DateTimeOffset.Now.AddHours(1), - Priority = CacheItemPriority.NotRemovable + Priority = CacheItemPriority.NeverRemove }; private ComplexTestObject testObject = new ComplexTestObject(); @@ -67,63 +67,63 @@ public void AddComplexObjectThenGetReturnsCachedObject() public void AddEmptyKeyThrowsException() { Action act = () => sut.Add("", new object()); - act.ShouldThrow(); + act.Should().Throw(); } [Test] public void AddEmptyKeyThrowsExceptionWithExpiration() { Action act = () => sut.Add("", new object(), DateTimeOffset.Now.AddHours(1)); - act.ShouldThrow(); + act.Should().Throw(); } [Test] public void AddEmptyKeyThrowsExceptionWithPolicy() { - Action act = () => sut.Add("", new object(), new CacheItemPolicy()); - act.ShouldThrow(); + Action act = () => sut.Add("", new object(), new MemoryCacheEntryOptions()); + act.Should().Throw(); } [Test] public void AddEmptyKeyThrowsExceptionWithSliding() { Action act = () => sut.Add("", new object(), new TimeSpan(1000)); - act.ShouldThrow(); + act.Should().Throw(); } [Test] public void AddNullKeyThrowsException() { Action act = () => sut.Add(null, new object()); - act.ShouldThrow(); + act.Should().Throw(); } [Test] public void AddNullKeyThrowsExceptionWithExpiration() { Action act = () => sut.Add(null, new object(), DateTimeOffset.Now.AddHours(1)); - act.ShouldThrow(); + act.Should().Throw(); } [Test] public void AddNullKeyThrowsExceptionWithPolicy() { - Action act = () => sut.Add(null, new object(), new CacheItemPolicy()); - act.ShouldThrow(); + Action act = () => sut.Add(null, new object(), new MemoryCacheEntryOptions()); + act.Should().Throw(); } [Test] public void AddNullKeyThrowsExceptionWithSliding() { Action act = () => sut.Add(null, new object(), new TimeSpan(1000)); - act.ShouldThrow(); + act.Should().Throw(); } [Test] public void AddNullThrowsException() { Action act = () => sut.Add(TestKey, null); - act.ShouldThrow(); + act.Should().Throw(); } [Test] @@ -151,7 +151,7 @@ public void AddWithOffsetThatExpiresReturnsNull() [Test] public void AddWithPolicyReturnsCachedItem() { - sut.Add(TestKey, "testObject", new CacheItemPolicy()); + sut.Add(TestKey, "testObject", new MemoryCacheEntryOptions()); Assert.AreEqual("testObject", sut.Get(TestKey)); } @@ -182,7 +182,7 @@ public void GetCachedNullableStructTypeParamReturnsType() public void GetEmptyKeyThrowsException() { Action act = () => sut.Get(""); - act.ShouldThrow(); + act.Should().Throw(); } [Test] @@ -217,7 +217,7 @@ public void GetFromCacheTwiceAtSameTimeOnlyAddsOnce() public void GetNullKeyThrowsException() { Action act = () => sut.Get(null); - act.ShouldThrow(); + act.Should().Throw(); } [Test] @@ -378,29 +378,29 @@ public async Task GetOrAddAsyncWillNotAddIfExistingData() Assert.AreEqual(0, times); } - [Test, Timeout(20000)] - public async Task GetOrAddAsyncWithCallbackOnRemovedReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() - { - Func> fetch = () => Task.FromResult(123); - CacheEntryRemovedArguments removedCallbackArgs = null; - CacheEntryRemovedCallback callback = args => removedCallbackArgs = args; - await sut.GetOrAddAsync(TestKey, fetch, - new CacheItemPolicy - { - AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), - RemovedCallback = callback - }); + //[Test, MaxTime(20000)] + //public async Task GetOrAddAsyncWithCallbackOnRemovedReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() + //{ + // Func> fetch = () => Task.FromResult(123); + // CacheEntryRemovedArguments removedCallbackArgs = null; + // CacheEntryRemovedCallback callback = args => removedCallbackArgs = args; + // await sut.GetOrAddAsync(TestKey, fetch, + // new MemoryCacheEntryOptions + // { + // AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), + // PostEvictionCallbacks = { callback } + // }); - sut.Remove(TestKey); //force removed callback to fire - while (removedCallbackArgs == null) - Thread.Sleep(500); + // sut.Remove(TestKey); //force removed callback to fire + // while (removedCallbackArgs == null) + // Thread.Sleep(500); - var callbackResult = removedCallbackArgs.CacheItem.Value; - Assert.That(callbackResult, Is.AssignableTo>()); - var callbackResultValue = await (Task) removedCallbackArgs.CacheItem.Value; + // var callbackResult = removedCallbackArgs.CacheItem.Value; + // Assert.That(callbackResult, Is.AssignableTo>()); + // var callbackResultValue = await (Task) removedCallbackArgs.CacheItem.Value; - Assert.AreEqual(123, callbackResultValue); - } + // Assert.AreEqual(123, callbackResultValue); + //} [Test] public async Task GetOrAddAsyncWithOffsetWillAddAndReturnTaskOfCached() @@ -422,7 +422,7 @@ public async Task GetOrAddAsyncWithPolicyAndThenGetTaskObjectReturnsCorrectType( var item = testObject; Func> fetch = () => Task.FromResult(item); await sut.GetOrAddAsync(TestKey, fetch, - oneHourNonRemoveableCacheItemPolicy); + oneHourNonRemoveableMemoryCacheEntryOptions); var actual = await sut.Get>(TestKey); Assert.That(actual, Is.EqualTo(item)); } @@ -433,7 +433,7 @@ public async Task GetOrAddAyncAllowsCachingATask() var cachedResult = testObject; Func> fetchAsync = () => Task.FromResult(cachedResult); - var actualResult = await sut.GetOrAddAsync(TestKey, fetchAsync, oneHourNonRemoveableCacheItemPolicy); + var actualResult = await sut.GetOrAddAsync(TestKey, fetchAsync, oneHourNonRemoveableMemoryCacheEntryOptions); Assert.That(actualResult, Is.EqualTo(cachedResult)); } @@ -493,25 +493,25 @@ public void GetOrAddWillNotAddIfExistingData() Assert.AreEqual(0, times); } - [Test, Timeout(20000)] - public void GetOrAddWithCallbackOnRemovedReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() - { - Func fetch = () => 123; - CacheEntryRemovedArguments removedCallbackArgs = null; - CacheEntryRemovedCallback callback = args => removedCallbackArgs = args; - sut.GetOrAdd(TestKey, fetch, - new CacheItemPolicy - { - AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), - RemovedCallback = callback - }); + //[Test, Timeout(20000)] + //public void GetOrAddWithCallbackOnRemovedReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() + //{ + // Func fetch = () => 123; + // CacheEntryRemovedArguments removedCallbackArgs = null; + // CacheEntryRemovedCallback callback = args => removedCallbackArgs = args; + // sut.GetOrAdd(TestKey, fetch, + // new MemoryCacheEntryOptions + // { + // AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), + // RemovedCallback = callback + // }); - sut.Remove(TestKey); //force removed callback to fire - while (removedCallbackArgs == null) - Thread.Sleep(500); + // sut.Remove(TestKey); //force removed callback to fire + // while (removedCallbackArgs == null) + // Thread.Sleep(500); - Assert.AreEqual(123, removedCallbackArgs.CacheItem.Value); - } + // Assert.AreEqual(123, removedCallbackArgs.CacheItem.Value); + //} [Test] public void GetOrAddWithOffsetWillAddAndReturnCached() @@ -532,7 +532,7 @@ public void GetOrAddWithPolicyAndThenGetObjectReturnsCorrectType() { Func fetch = () => testObject; sut.GetOrAdd(TestKey, fetch, - oneHourNonRemoveableCacheItemPolicy); + oneHourNonRemoveableMemoryCacheEntryOptions); var actual = sut.Get(TestKey); Assert.IsNotNull(actual); } @@ -541,7 +541,7 @@ public void GetOrAddWithPolicyAndThenGetObjectReturnsCorrectType() public void GetOrAddWithPolicyAndThenGetValueObjectReturnsCorrectType() { Func fetch = () => 123; - sut.GetOrAdd(TestKey, fetch, oneHourNonRemoveableCacheItemPolicy); + sut.GetOrAdd(TestKey, fetch, oneHourNonRemoveableMemoryCacheEntryOptions); var actual = sut.Get(TestKey); Assert.AreEqual(123, actual); } @@ -556,41 +556,41 @@ public void GetOrAddWithPolicyWillAddOnFirstCallButReturnCachedOnSecond() { times++; return new DateTime(2001, 01, 01); - }, oneHourNonRemoveableCacheItemPolicy); + }, oneHourNonRemoveableMemoryCacheEntryOptions); var expectedSecond = sut.GetOrAdd(TestKey, () => { times++; return new DateTime(2002, 01, 01); - }, oneHourNonRemoveableCacheItemPolicy); + }, oneHourNonRemoveableMemoryCacheEntryOptions); Assert.AreEqual(2001, expectedFirst.Year); Assert.AreEqual(2001, expectedSecond.Year); Assert.AreEqual(1, times); } - [Test, Timeout(20000)] - public void GetOrAddWithPolicyWithCallbackOnRemovedReturnsTheOriginalCachedObject() - { - Func fetch = () => 123; - CacheEntryRemovedArguments removedCallbackArgs = null; - CacheEntryRemovedCallback callback = args => removedCallbackArgs = args; + //[Test, MaxTime(20000)] + //public void GetOrAddWithPolicyWithCallbackOnRemovedReturnsTheOriginalCachedObject() + //{ + // Func fetch = () => 123; + // CacheEntryRemovedArguments removedCallbackArgs = null; + // CacheEntryRemovedCallback callback = args => removedCallbackArgs = args; - sut.GetOrAdd(TestKey, fetch, - new CacheItemPolicy - { - AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), - RemovedCallback = callback - }); - sut.Get(TestKey); + // sut.GetOrAdd(TestKey, fetch, + // new MemoryCacheEntryOptions + // { + // AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), + // RemovedCallback = callback + // }); + // sut.Get(TestKey); - sut.Remove(TestKey); //force removed callback to fire - while (removedCallbackArgs == null) - Thread.Sleep(500); + // sut.Remove(TestKey); //force removed callback to fire + // while (removedCallbackArgs == null) + // Thread.Sleep(500); - Assert.AreEqual(123, removedCallbackArgs.CacheItem.Value); - } + // Assert.AreEqual(123, removedCallbackArgs.CacheItem.Value); + //} [Test] public void GetWithClassTypeParamReturnsType() @@ -664,7 +664,7 @@ public void RemovedItemCannotBeRetrievedFromCache() } [Test] - [Timeout(1000)] + [MaxTime(1000)] public void GetOrAddAsyncWithALongTaskReturnsBeforeTaskCompletes() { var cachedResult = testObject; diff --git a/LazyCache.UnitTests/LazyCache.UnitTests.csproj b/LazyCache.UnitTests/LazyCache.UnitTests.csproj index 77a9fcd..b5e5ad0 100644 --- a/LazyCache.UnitTests/LazyCache.UnitTests.csproj +++ b/LazyCache.UnitTests/LazyCache.UnitTests.csproj @@ -1,83 +1,20 @@ - - - - - Debug - AnyCPU - {7F6C8799-CA9E-43BD-AE97-72C31BB4E106} - Library - Properties - LazyCache.UnitTests - LazyCache.UnitTests - v4.5 - 512 - ..\ - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - ..\packages\FluentAssertions.4.13.1\lib\net45\FluentAssertions.dll - True - - - ..\packages\FluentAssertions.4.13.1\lib\net45\FluentAssertions.Core.dll - True - - - ..\packages\NUnit.3.4.1\lib\net45\nunit.framework.dll - True - - - - - - - - - - - - - - - - - - {E6A1EF20-94AD-4A1C-9A89-3B2FA8AD8EC7} - LazyCache - - - - - - - - - - + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + \ No newline at end of file diff --git a/LazyCache.UnitTests/Properties/AssemblyInfo.cs b/LazyCache.UnitTests/Properties/AssemblyInfo.cs deleted file mode 100644 index f0e46fc..0000000 --- a/LazyCache.UnitTests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("LazyCache.UnitTests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("LazyCache.UnitTests")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("bc1a33b7-98ff-483f-8f0c-f62dda71e3f1")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/LazyCache.UnitTests/packages.config b/LazyCache.UnitTests/packages.config deleted file mode 100644 index 0814980..0000000 --- a/LazyCache.UnitTests/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index ac59d88..84c6c40 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -1,21 +1,18 @@ using System; -using System.Runtime.Caching; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; namespace LazyCache { public class CachingService : IAppCache { - public CachingService() : this(MemoryCache.Default) + public CachingService() : this(new MemoryCache(new MemoryCacheOptions())) { } - public CachingService(ObjectCache cache) + public CachingService(IMemoryCache cache) { - if (cache == null) - throw new ArgumentNullException(nameof(cache)); - - ObjectCache = cache; + MemoryCache = cache ?? throw new ArgumentNullException(nameof(cache)); DefaultCacheDuration = 60*20; } @@ -33,28 +30,28 @@ public void Add(string key, T item) public void Add(string key, T item, DateTimeOffset expires) { - Add(key, item, new CacheItemPolicy {AbsoluteExpiration = expires}); + Add(key, item, new MemoryCacheEntryOptions { AbsoluteExpiration = expires}); } public void Add(string key, T item, TimeSpan slidingExpiration) { - Add(key, item, new CacheItemPolicy {SlidingExpiration = slidingExpiration}); + Add(key, item, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } - public void Add(string key, T item, CacheItemPolicy policy) + public void Add(string key, T item, MemoryCacheEntryOptions policy) { if (item == null) throw new ArgumentNullException(nameof(item)); ValidateKey(key); - ObjectCache.Set(key, item, policy); + MemoryCache.Set(key, item, policy); } public T Get(string key) { ValidateKey(key); - var item = ObjectCache[key]; + var item = MemoryCache.Get(key); return UnwrapLazy(item); } @@ -64,7 +61,7 @@ public async Task GetAsync(string key) { ValidateKey(key); - var item = ObjectCache[key]; + var item = MemoryCache.Get(key); return await UnwrapAsyncLazys(item); } @@ -76,66 +73,68 @@ public T GetOrAdd(string key, Func addItemFactory) } - public async Task GetOrAddAsync(string key, Func> addItemFactory, CacheItemPolicy policy) + public async Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy) { ValidateKey(key); - var newLazyCacheItem = new AsyncLazy(addItemFactory); - EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(policy); - var existingCacheItem = ObjectCache.AddOrGetExisting(key, newLazyCacheItem, policy); - - if (existingCacheItem != null) - return await UnwrapAsyncLazys(existingCacheItem); + var cacheItem = MemoryCache.GetOrCreate(key, entry => + { + entry.SetOptions(policy); + var value = new AsyncLazy(addItemFactory); + return (object) value; + } + ); try { - var result = newLazyCacheItem.Value; + var result = UnwrapAsyncLazys(cacheItem); if (result.IsCanceled || result.IsFaulted) - ObjectCache.Remove(key); + MemoryCache.Remove(key); return await result; } catch //addItemFactory errored so do not cache the exception { - ObjectCache.Remove(key); + MemoryCache.Remove(key); throw; } } public T GetOrAdd(string key, Func addItemFactory, DateTimeOffset expires) { - return GetOrAdd(key, addItemFactory, new CacheItemPolicy {AbsoluteExpiration = expires}); + return GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); } public T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpiration) { - return GetOrAdd(key, addItemFactory, new CacheItemPolicy {SlidingExpiration = slidingExpiration}); + return GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } - public T GetOrAdd(string key, Func addItemFactory, CacheItemPolicy policy) + public T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy) { ValidateKey(key); - var newLazyCacheItem = new Lazy(addItemFactory); - EnsureRemovedCallbackDoesNotReturnTheLazy(policy); - var existingCacheItem = ObjectCache.AddOrGetExisting(key, newLazyCacheItem, policy); - - if (existingCacheItem != null) - return UnwrapLazy(existingCacheItem); + var cacheItem = MemoryCache.GetOrCreate(key, entry => + { + entry.SetOptions(policy); + var value = new Lazy(addItemFactory); + return (object)value; + } + ); try { - return newLazyCacheItem.Value; + return UnwrapLazy(cacheItem); } catch //addItemFactory errored so do not cache the exception { - ObjectCache.Remove(key); + MemoryCache.Remove(key); throw; } } @@ -144,10 +143,10 @@ public T GetOrAdd(string key, Func addItemFactory, CacheItemPolicy policy) public void Remove(string key) { ValidateKey(key); - ObjectCache.Remove(key); + MemoryCache.Remove(key); } - public ObjectCache ObjectCache { get; } + public IMemoryCache MemoryCache { get; } public async Task GetOrAddAsync(string key, Func> addItemFactory) { @@ -156,12 +155,12 @@ public async Task GetOrAddAsync(string key, Func> addItemFactory) public async Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires) { - return await GetOrAddAsync(key, addItemFactory, new CacheItemPolicy {AbsoluteExpiration = expires}); + return await GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); } public async Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration) { - return await GetOrAddAsync(key, addItemFactory, new CacheItemPolicy {SlidingExpiration = slidingExpiration}); + return await GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } private static T UnwrapLazy(object item) @@ -175,7 +174,7 @@ private static T UnwrapLazy(object item) var asyncLazy = item as AsyncLazy; if (asyncLazy != null) - return asyncLazy.Value.Result; + return asyncLazy.Value.ConfigureAwait(false).GetAwaiter().GetResult(); var task = item as Task; if (task != null) @@ -204,36 +203,40 @@ private static async Task UnwrapAsyncLazys(object item) return default(T); } - private static void EnsureRemovedCallbackDoesNotReturnTheLazy(CacheItemPolicy policy) + private static void EnsureRemovedCallbackDoesNotReturnTheLazy(MemoryCacheEntryOptions policy) { - if (policy?.RemovedCallback != null) + if (policy?.PostEvictionCallbacks != null) { - var originallCallback = policy.RemovedCallback; - policy.RemovedCallback = args => + foreach (var item in policy.PostEvictionCallbacks) { - //unwrap the cache item in a callback given one is specified - var item = args?.CacheItem?.Value as Lazy; - if (item != null) - args.CacheItem.Value = item.IsValueCreated ? item.Value : default(T); - originallCallback(args); - }; + var originallCallback = item.EvictionCallback; + item.EvictionCallback = (key, value, reason, state) => + { + //unwrap the cache item in a callback given one is specified + if (value is Lazy cacheItem) + value = cacheItem.IsValueCreated ? cacheItem.Value : default(T); ; + originallCallback(key, value, reason, state); + }; + } } } - private static void EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(CacheItemPolicy policy) + private static void EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(MemoryCacheEntryOptions policy) { - if (policy?.RemovedCallback != null) - { - var originallCallback = policy.RemovedCallback; - policy.RemovedCallback = args => + if (policy?.PostEvictionCallbacks != null) + foreach (var item in policy.PostEvictionCallbacks) { - //unwrap the cache item in a callback given one is specified - var item = args?.CacheItem?.Value as AsyncLazy; - if (item != null) - args.CacheItem.Value = item.IsValueCreated ? item.Value : Task.FromResult(default(T)); - originallCallback(args); - }; - } + var originalCallback = item.EvictionCallback; + item.EvictionCallback = (key, value, reason, state) => + { + //unwrap the cache item in a callback given one is specified + if (value is AsyncLazy cacheItem) + { + value = cacheItem.IsValueCreated ? cacheItem.Value : Task.FromResult(default(T)); + } + originalCallback(key, value, reason, state); + }; + } } private void ValidateKey(string key) diff --git a/LazyCache/IAppCache.cs b/LazyCache/IAppCache.cs index b4d65a8..c4cc1f1 100644 --- a/LazyCache/IAppCache.cs +++ b/LazyCache/IAppCache.cs @@ -1,28 +1,28 @@ using System; -using System.Runtime.Caching; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; namespace LazyCache { public interface IAppCache { - ObjectCache ObjectCache { get; } + IMemoryCache MemoryCache { get; } void Add(string key, T item); void Add(string key, T item, DateTimeOffset absoluteExpiration); void Add(string key, T item, TimeSpan slidingExpiration); - void Add(string key, T item, CacheItemPolicy policy); + void Add(string key, T item, MemoryCacheEntryOptions policy); T Get(string key); T GetOrAdd(string key, Func addItemFactory); T GetOrAdd(string key, Func addItemFactory, DateTimeOffset absoluteExpiration); T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpiration); - T GetOrAdd(string key, Func addItemFactory, CacheItemPolicy policy); + T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy); void Remove(string key); Task GetOrAddAsync(string key, Func> addItemFactory); - Task GetOrAddAsync(string key, Func> addItemFactory, CacheItemPolicy policy); + Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy); Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires); Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration); Task GetAsync(string key); diff --git a/LazyCache/LazyCache.csproj b/LazyCache/LazyCache.csproj index 46650a0..a663038 100644 --- a/LazyCache/LazyCache.csproj +++ b/LazyCache/LazyCache.csproj @@ -1,66 +1,12 @@ - - - - - Debug - AnyCPU - {E6A1EF20-94AD-4A1C-9A89-3B2FA8AD8EC7} - Library - Properties - LazyCache - LazyCache - v4.5 - 512 - ..\ - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - - - - - - - - - - - - - - - - - - Designer - - - - - \ No newline at end of file + + + + netstandard2.0 + + + + + + + + diff --git a/LazyCache/Mocks/MockCachingService.cs b/LazyCache/Mocks/MockCachingService.cs index 8f6946d..f547c86 100644 --- a/LazyCache/Mocks/MockCachingService.cs +++ b/LazyCache/Mocks/MockCachingService.cs @@ -1,6 +1,6 @@ using System; -using System.Runtime.Caching; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; namespace LazyCache.Mocks { @@ -37,7 +37,7 @@ public void Remove(string key) { } - public Task GetOrAddAsync(string key, Func> addItemFactory, CacheItemPolicy policy) + public Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy) { return addItemFactory.Invoke(); } @@ -62,14 +62,14 @@ public Task GetAsync(string key) return Task.FromResult(default(T)); } - public ObjectCache ObjectCache => null; + public IMemoryCache MemoryCache => null; public void Add(string key, T item, TimeSpan slidingExpiration) { } - public void Add(string key, T item, CacheItemPolicy policy) + public void Add(string key, T item, MemoryCacheEntryOptions policy) { } @@ -78,7 +78,7 @@ public T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpirat return addItemFactory.Invoke(); } - public T GetOrAdd(string key, Func addItemFactory, CacheItemPolicy policy) + public T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy) { return addItemFactory.Invoke(); } diff --git a/LazyCache/Properties/AssemblyInfo.cs b/LazyCache/Properties/AssemblyInfo.cs deleted file mode 100644 index f564453..0000000 --- a/LazyCache/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("LazyCache")] -[assembly: AssemblyDescription(@"Lazy cache is a simple,thread safe in-memory caching service")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("LazyCache")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("86194e2f-3263-4ecc-b095-7c789779ba02")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("0.7.1.*")] \ No newline at end of file diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 1a61396..1134385 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,11 @@ # Release notes for LazyCache # +## Version 2.0.0 +- *BREAKING CHANGE* Upgrade to netstandard2.0 +- *BREAKING CHANGE* Change underlying cache from System.Runtime.Caching to Microsft.Extension.Caching.Memory +- *BREAKING CHANGE* Removed IAppCache.ObjectCache (change to IAppCache.MemoryCache) +- *BREAKING CHANGE* changed from CacheItemPolicy to MemoryCacheEntryOptions. Now uses PostEvictionCallbacks. + ## Version 0.7.1 - Fix async/sync interopability bug, see https://github.com/alastairtree/LazyCache/issues/12 From 29a7683b9c4b966930c38c656d90f17ee684bb65 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 25 Feb 2018 23:43:39 +0000 Subject: [PATCH 02/39] task: add basic build script --- appveyor.yml | 19 +++++++++++++++++++ build.ps1 | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 appveyor.yml create mode 100644 build.ps1 diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..590c30b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,19 @@ +version: 2.0.0.{build}-beta01 +configuration: Release +assembly_info: + patch: true + file: '**\AssemblyInfo.*' + assembly_version: '{version}' + assembly_file_version: '{version}' + assembly_informational_version: '{version}' +dotnet_csproj: + patch: true + file: '**\*.csproj' + version: '{version}' + package_version: '{version}' + assembly_version: '{version}' + file_version: '{version}' + informational_version: '{version}' +build_script: +- ps: '& .\build.ps1' +deploy: off \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..11b6bf4 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,2 @@ +dotnet restore +dotnet build -c Release \ No newline at end of file From 4ba893bcaf3dbd2fc6c5b810f111f8d43df8dbb8 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 25 Feb 2018 23:45:21 +0000 Subject: [PATCH 03/39] feat: porting sample app. caching not working --- .../CacheDatabaseQueriesApiSample.csproj | 20 +++ .../Controllers/DbQueriesController.cs | 6 +- .../Controllers/DbTimeController.cs | 42 +++++ .../DbTime.cs | 6 +- .../DbTimeContext.cs | 33 ++++ CacheDatabaseQueriesApiSample/Program.cs | 25 +++ .../Properties/launchSettings.json | 29 ++++ CacheDatabaseQueriesApiSample/Startup.cs | 45 +++++ .../appsettings.Development.json | 10 ++ .../appsettings.json | 15 ++ .../wwwroot}/index.html | 4 +- LazyCache.sln | 25 +-- LazyCache/LazyCache.csproj | 12 ++ LazyCache/LazyCache.nuspec | 19 --- .../ApiAsyncCachingSample.csproj | 155 ------------------ .../App_Start/WebApiConfig.cs | 15 -- .../Controllers/DbTimeController.cs | 46 ------ Samples/ApiAsyncCachingSample/Global.asax | 1 - Samples/ApiAsyncCachingSample/Global.asax.cs | 17 -- .../Models/DbTimeContext.cs | 27 --- .../Properties/AssemblyInfo.cs | 35 ---- .../ApiAsyncCachingSample/Web.Debug.config | 30 ---- .../ApiAsyncCachingSample/Web.Release.config | 31 ---- Samples/ApiAsyncCachingSample/Web.config | 63 ------- Samples/ApiAsyncCachingSample/packages.config | 11 -- 25 files changed, 255 insertions(+), 467 deletions(-) create mode 100644 CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj rename {Samples/ApiAsyncCachingSample => CacheDatabaseQueriesApiSample}/Controllers/DbQueriesController.cs (63%) create mode 100644 CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs rename {Samples/ApiAsyncCachingSample/Models => CacheDatabaseQueriesApiSample}/DbTime.cs (55%) create mode 100644 CacheDatabaseQueriesApiSample/DbTimeContext.cs create mode 100644 CacheDatabaseQueriesApiSample/Program.cs create mode 100644 CacheDatabaseQueriesApiSample/Properties/launchSettings.json create mode 100644 CacheDatabaseQueriesApiSample/Startup.cs create mode 100644 CacheDatabaseQueriesApiSample/appsettings.Development.json create mode 100644 CacheDatabaseQueriesApiSample/appsettings.json rename {Samples/ApiAsyncCachingSample => CacheDatabaseQueriesApiSample/wwwroot}/index.html (95%) delete mode 100644 LazyCache/LazyCache.nuspec delete mode 100644 Samples/ApiAsyncCachingSample/ApiAsyncCachingSample.csproj delete mode 100644 Samples/ApiAsyncCachingSample/App_Start/WebApiConfig.cs delete mode 100644 Samples/ApiAsyncCachingSample/Controllers/DbTimeController.cs delete mode 100644 Samples/ApiAsyncCachingSample/Global.asax delete mode 100644 Samples/ApiAsyncCachingSample/Global.asax.cs delete mode 100644 Samples/ApiAsyncCachingSample/Models/DbTimeContext.cs delete mode 100644 Samples/ApiAsyncCachingSample/Properties/AssemblyInfo.cs delete mode 100644 Samples/ApiAsyncCachingSample/Web.Debug.config delete mode 100644 Samples/ApiAsyncCachingSample/Web.Release.config delete mode 100644 Samples/ApiAsyncCachingSample/Web.config delete mode 100644 Samples/ApiAsyncCachingSample/packages.config diff --git a/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj b/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj new file mode 100644 index 0000000..8174edb --- /dev/null +++ b/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + diff --git a/Samples/ApiAsyncCachingSample/Controllers/DbQueriesController.cs b/CacheDatabaseQueriesApiSample/Controllers/DbQueriesController.cs similarity index 63% rename from Samples/ApiAsyncCachingSample/Controllers/DbQueriesController.cs rename to CacheDatabaseQueriesApiSample/Controllers/DbQueriesController.cs index 773e5b8..efca4e8 100644 --- a/Samples/ApiAsyncCachingSample/Controllers/DbQueriesController.cs +++ b/CacheDatabaseQueriesApiSample/Controllers/DbQueriesController.cs @@ -1,9 +1,9 @@ -using System.Web.Http; -using ApiAsyncCachingSample.Models; +using CacheDatabaseQueriesApiSample; +using Microsoft.AspNetCore.Mvc; namespace ApiAsyncCachingSample.Controllers { - public class DbQueriesController : ApiController + public class DbQueriesController : Controller { [HttpGet] [Route("api/dbQueries")] diff --git a/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs b/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs new file mode 100644 index 0000000..5d2aca5 --- /dev/null +++ b/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using LazyCache; +using Microsoft.AspNetCore.Mvc; + +namespace CacheDatabaseQueriesApiSample.Controllers +{ + public class DbTimeController : Controller + { + private readonly IAppCache cache; + private readonly string cacheKey = "DbTimeController.Get"; + private readonly DbTimeContext dbContext; + + + public DbTimeController(DbTimeContext context) + { + // this could (and should) be passed in using dependency injection + cache = new CachingService(); + dbContext = context; + } + + [HttpGet] + [Route("api/dbtime")] + public DbTime Get() + { + Func cacheableAsyncFunc = () => dbContext.GeDbTime(); + + var cachedDatabaseTime = cache.GetOrAdd(cacheKey, cacheableAsyncFunc); + + return cachedDatabaseTime; + } + + [HttpDelete] + [Route("api/dbtime")] + public IActionResult DeleteFromCache() + { + cache.Remove(cacheKey); + var friendlyMessage = new {Message = $"Item with key '{cacheKey}' removed from server in-memory cache"}; + return Ok(friendlyMessage); + } + } +} \ No newline at end of file diff --git a/Samples/ApiAsyncCachingSample/Models/DbTime.cs b/CacheDatabaseQueriesApiSample/DbTime.cs similarity index 55% rename from Samples/ApiAsyncCachingSample/Models/DbTime.cs rename to CacheDatabaseQueriesApiSample/DbTime.cs index 5d7c4ac..253cd92 100644 --- a/Samples/ApiAsyncCachingSample/Models/DbTime.cs +++ b/CacheDatabaseQueriesApiSample/DbTime.cs @@ -1,6 +1,6 @@ using System; -namespace ApiAsyncCachingSample.Models +namespace CacheDatabaseQueriesApiSample { public class DbTime { @@ -14,7 +14,9 @@ public DbTime() } - public DateTime TimeNowInTheDatabase { get; set; } + public virtual int id { get; set; } + + public virtual DateTime TimeNowInTheDatabase { get; set; } } diff --git a/CacheDatabaseQueriesApiSample/DbTimeContext.cs b/CacheDatabaseQueriesApiSample/DbTimeContext.cs new file mode 100644 index 0000000..6147748 --- /dev/null +++ b/CacheDatabaseQueriesApiSample/DbTimeContext.cs @@ -0,0 +1,33 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace CacheDatabaseQueriesApiSample +{ + public class DbTimeContext : DbContext + { + private static int databaseRequestCounter; //just for demo - don't use static fields for statistics! + + public DbTimeContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Times { get; set; } + + public static int DatabaseRequestCounter() + { + return databaseRequestCounter; + } + + public DbTime GeDbTime() + { + // get the current time from SQL server right now asynchronously (simulating a slow query) + var result = Times.FromSql("WAITFOR DELAY '00:00:00:500'; SELECT 1 as ID, GETDATE() as [TimeNowInTheDatabase]") + .Single(); + + databaseRequestCounter++; + + return result; + } + } +} \ No newline at end of file diff --git a/CacheDatabaseQueriesApiSample/Program.cs b/CacheDatabaseQueriesApiSample/Program.cs new file mode 100644 index 0000000..8309742 --- /dev/null +++ b/CacheDatabaseQueriesApiSample/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace CacheDatabaseQueriesApiSample +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/CacheDatabaseQueriesApiSample/Properties/launchSettings.json b/CacheDatabaseQueriesApiSample/Properties/launchSettings.json new file mode 100644 index 0000000..d88345d --- /dev/null +++ b/CacheDatabaseQueriesApiSample/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52671/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "CacheDatabaseQueriesApiSample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:52672/" + } + } +} diff --git a/CacheDatabaseQueriesApiSample/Startup.cs b/CacheDatabaseQueriesApiSample/Startup.cs new file mode 100644 index 0000000..48ec8cc --- /dev/null +++ b/CacheDatabaseQueriesApiSample/Startup.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace CacheDatabaseQueriesApiSample +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + var connection = @"Server=(localdb)\projectsv13;Database=Master;Trusted_Connection=True;ConnectRetryCount=0"; + services.AddDbContext(options => options.UseSqlServer(connection)); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseStaticFiles(); + app.UseDefaultFiles(); + app.UseMvc(); + } + } +} diff --git a/CacheDatabaseQueriesApiSample/appsettings.Development.json b/CacheDatabaseQueriesApiSample/appsettings.Development.json new file mode 100644 index 0000000..f334029 --- /dev/null +++ b/CacheDatabaseQueriesApiSample/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/CacheDatabaseQueriesApiSample/appsettings.json b/CacheDatabaseQueriesApiSample/appsettings.json new file mode 100644 index 0000000..647f1a9 --- /dev/null +++ b/CacheDatabaseQueriesApiSample/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/Samples/ApiAsyncCachingSample/index.html b/CacheDatabaseQueriesApiSample/wwwroot/index.html similarity index 95% rename from Samples/ApiAsyncCachingSample/index.html rename to CacheDatabaseQueriesApiSample/wwwroot/index.html index 773a017..1a480ef 100644 --- a/Samples/ApiAsyncCachingSample/index.html +++ b/CacheDatabaseQueriesApiSample/wwwroot/index.html @@ -70,7 +70,7 @@

Sample app to demonstrate using an async cache in your API to save database }, updateLogWithTime: function (dbTime, duration) { - var message = "API reports that the time now in the database is " + dbTime.TimeNowInTheDatabase + " (" + duration + "ms)"; + var message = "API reports that the time now in the database is " + dbTime.timeNowInTheDatabase + " (" + duration + "ms)"; app.log(message); }, @@ -80,7 +80,7 @@

Sample app to demonstrate using an async cache in your API to save database url: "/api/dbtime" }) .done(function (data) { - app.log(data.Message); + app.log(data.message); }); }, diff --git a/LazyCache.sln b/LazyCache.sln index a729d3f..1c33f81 100644 --- a/LazyCache.sln +++ b/LazyCache.sln @@ -1,21 +1,23 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2024 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyCache", "LazyCache\LazyCache.csproj", "{E6A1EF20-94AD-4A1C-9A89-3B2FA8AD8EC7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LazyCache", "LazyCache\LazyCache.csproj", "{E6A1EF20-94AD-4A1C-9A89-3B2FA8AD8EC7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyCache.UnitTests", "LazyCache.UnitTests\LazyCache.UnitTests.csproj", "{7F6C8799-CA9E-43BD-AE97-72C31BB4E106}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LazyCache.UnitTests", "LazyCache.UnitTests\LazyCache.UnitTests.csproj", "{7F6C8799-CA9E-43BD-AE97-72C31BB4E106}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "info", "info", "{81C0E096-59B7-4129-851B-8183FDB9B02B}" ProjectSection(SolutionItems) = preProject + appveyor.yml = appveyor.yml + build.ps1 = build.ps1 Readme.md = Readme.md ReleaseNotes.md = ReleaseNotes.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{335BA426-C839-4996-8476-F3EE4056C40E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiAsyncCachingSample", "Samples\ApiAsyncCachingSample\ApiAsyncCachingSample.csproj", "{FB8EDC6D-CD41-47AA-9758-D8D796F7728C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheDatabaseQueriesApiSample", "CacheDatabaseQueriesApiSample\CacheDatabaseQueriesApiSample.csproj", "{5D6A88DD-230C-4057-B8EB-A987FF4F29DB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -31,15 +33,18 @@ Global {7F6C8799-CA9E-43BD-AE97-72C31BB4E106}.Debug|Any CPU.Build.0 = Debug|Any CPU {7F6C8799-CA9E-43BD-AE97-72C31BB4E106}.Release|Any CPU.ActiveCfg = Release|Any CPU {7F6C8799-CA9E-43BD-AE97-72C31BB4E106}.Release|Any CPU.Build.0 = Release|Any CPU - {FB8EDC6D-CD41-47AA-9758-D8D796F7728C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FB8EDC6D-CD41-47AA-9758-D8D796F7728C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FB8EDC6D-CD41-47AA-9758-D8D796F7728C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FB8EDC6D-CD41-47AA-9758-D8D796F7728C}.Release|Any CPU.Build.0 = Release|Any CPU + {5D6A88DD-230C-4057-B8EB-A987FF4F29DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D6A88DD-230C-4057-B8EB-A987FF4F29DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D6A88DD-230C-4057-B8EB-A987FF4F29DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D6A88DD-230C-4057-B8EB-A987FF4F29DB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {FB8EDC6D-CD41-47AA-9758-D8D796F7728C} = {335BA426-C839-4996-8476-F3EE4056C40E} + {5D6A88DD-230C-4057-B8EB-A987FF4F29DB} = {335BA426-C839-4996-8476-F3EE4056C40E} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5040E431-0FAA-4DC7-A678-D218CD57D542} EndGlobalSection EndGlobal diff --git a/LazyCache/LazyCache.csproj b/LazyCache/LazyCache.csproj index a663038..397df86 100644 --- a/LazyCache/LazyCache.csproj +++ b/LazyCache/LazyCache.csproj @@ -2,6 +2,18 @@ netstandard2.0 + true + 2.0.0 + https://github.com/alastairtree + https://github.com/alastairtree + Lazy cache is a simple, thread safe in-memory caching service + https://github.com/alastairtree/LazyCache + Copyright 2014 - 2018 Alastair Crabtree + https://github.com/alastairtree/LazyCache/blob/master/LICENSE + https://raw.githubusercontent.com/alastairtree/LazyCache/master/artwork/logo-128.png + https://github.com/alastairtree/LazyCache + Caching Performance Speed In-memory ObjectCache Generics ServiceCacheing Lazy Cache Lazy-Load MemoryCache CachingService AppCache ApplicationCache Memcached + See https://raw.githubusercontent.com/alastairtree/LazyCache/master/ReleaseNotes.md diff --git a/LazyCache/LazyCache.nuspec b/LazyCache/LazyCache.nuspec deleted file mode 100644 index 19ac552..0000000 --- a/LazyCache/LazyCache.nuspec +++ /dev/null @@ -1,19 +0,0 @@ - - - - $id$ - $version$ - $title$ - https://github.com/alastairtree - https://github.com/alastairtree - https://github.com/alastairtree/LazyCache/blob/master/LICENSE - https://github.com/alastairtree/LazyCache - https://raw.githubusercontent.com/alastairtree/LazyCache/master/artwork/logo-128.png - false - Lazy cache is a simple,thread safe in-memory caching service - See https://raw.githubusercontent.com/alastairtree/LazyCache/master/ReleaseNotes.md - Copyright 2014 - Lazy cache is a simple in-memory caching service for .net and c sharp. It has a developer friendly generics based API, and providing a thread safe cache implementation that guarantees to only execute your cachable delegates once (it's lazy!). Under the hood it leverages ObjectCache and Lazy to provide performance and reliability in heavy load scenarios. For more info see https://github.com/alastairtree/LazyCache - Caching Performance Speed In-memory ObjectCache Generics ServiceCacheing Lazy Cache Lazy-Load MemoryCache CachingService AppCache ApplicationCache Memcached - - \ No newline at end of file diff --git a/Samples/ApiAsyncCachingSample/ApiAsyncCachingSample.csproj b/Samples/ApiAsyncCachingSample/ApiAsyncCachingSample.csproj deleted file mode 100644 index b1e3c2f..0000000 --- a/Samples/ApiAsyncCachingSample/ApiAsyncCachingSample.csproj +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - Debug - AnyCPU - - - 2.0 - {FB8EDC6D-CD41-47AA-9758-D8D796F7728C} - {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} - Library - Properties - ApiAsyncCachingSample - ApiAsyncCachingSample - v4.5 - true - - - - - - - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\ - TRACE - prompt - 4 - - - - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll - True - - - ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll - True - - - ..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.1.0.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll - True - - - - - - - - - - - - - - - - - - - - - - - ..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll - - - ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll - - - ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll - - - ..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll - - - - - - - Designer - - - - - - - - Global.asax - - - - - - - - - Web.config - - - Web.config - - - - - - {E6A1EF20-94AD-4A1C-9A89-3B2FA8AD8EC7} - LazyCache - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - - - - - True - True - 15586 - / - http://localhost:15586/ - False - False - - - False - - - - - - \ No newline at end of file diff --git a/Samples/ApiAsyncCachingSample/App_Start/WebApiConfig.cs b/Samples/ApiAsyncCachingSample/App_Start/WebApiConfig.cs deleted file mode 100644 index fecdb7a..0000000 --- a/Samples/ApiAsyncCachingSample/App_Start/WebApiConfig.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Http; - -namespace ApiAsyncCachingSample -{ - public static class WebApiConfig - { - public static void Register(HttpConfiguration config) - { - config.MapHttpAttributeRoutes(); - } - } -} diff --git a/Samples/ApiAsyncCachingSample/Controllers/DbTimeController.cs b/Samples/ApiAsyncCachingSample/Controllers/DbTimeController.cs deleted file mode 100644 index a5742df..0000000 --- a/Samples/ApiAsyncCachingSample/Controllers/DbTimeController.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Web.Http; -using ApiAsyncCachingSample.Models; -using LazyCache; - -namespace ApiAsyncCachingSample.Controllers -{ - public class DbTimeController : ApiController - { - private readonly IAppCache cache; - private readonly string cacheKey = "DbTimeController.Get"; - private readonly DbTimeContext dbContext; - - - public DbTimeController() - { - // these could be passed in using dependency injection - dbContext = new DbTimeContext(); - cache = new CachingService(); - } - - [HttpGet] - [Route("api/dbtime")] - public async Task Get() - { - Func> cacheableAsyncFunc = () => dbContext.GeDbTime(); - - var cachedDatabaseTime = await cache.GetOrAddAsync(cacheKey, cacheableAsyncFunc); - - return cachedDatabaseTime; - - // Or instead just do it all in one line if you prefer - // return await cache.GetOrAddAsync(cacheKey, dbContext.GeDbTime); - } - - [HttpDelete] - [Route("api/dbtime")] - public IHttpActionResult DeleteFromCache() - { - cache.Remove(cacheKey); - var friendlyMessage = new {Message = $"Item with key '{cacheKey}' removed from server in-memory cache"}; - return Ok(friendlyMessage); - } - } -} \ No newline at end of file diff --git a/Samples/ApiAsyncCachingSample/Global.asax b/Samples/ApiAsyncCachingSample/Global.asax deleted file mode 100644 index ecc932c..0000000 --- a/Samples/ApiAsyncCachingSample/Global.asax +++ /dev/null @@ -1 +0,0 @@ -<%@ Application Codebehind="Global.asax.cs" Inherits="ApiAsyncCachingSample.WebApiApplication" Language="C#" %> diff --git a/Samples/ApiAsyncCachingSample/Global.asax.cs b/Samples/ApiAsyncCachingSample/Global.asax.cs deleted file mode 100644 index a4351a2..0000000 --- a/Samples/ApiAsyncCachingSample/Global.asax.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Http; -using System.Web.Routing; - -namespace ApiAsyncCachingSample -{ - public class WebApiApplication : System.Web.HttpApplication - { - protected void Application_Start() - { - GlobalConfiguration.Configure(WebApiConfig.Register); - } - } -} diff --git a/Samples/ApiAsyncCachingSample/Models/DbTimeContext.cs b/Samples/ApiAsyncCachingSample/Models/DbTimeContext.cs deleted file mode 100644 index 12dbfb0..0000000 --- a/Samples/ApiAsyncCachingSample/Models/DbTimeContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Data.Entity; -using System.Threading.Tasks; - -namespace ApiAsyncCachingSample.Models -{ - public class DbTimeContext : DbContext - { - private static int databaseRequestCounter = 0; //just for demo - don't use static fields for statistics! - - public static int DatabaseRequestCounter() - { - return databaseRequestCounter; - } - - public async Task GeDbTime() - { - // get the current time from SQL server right now asynchronously (simulating a slow query) - var result = await Database - .SqlQuery("WAITFOR DELAY '00:00:00:500'; SELECT GETDATE() as [TimeNowInTheDatabase]") - .SingleAsync(); - - databaseRequestCounter++; - - return result; - } - } -} \ No newline at end of file diff --git a/Samples/ApiAsyncCachingSample/Properties/AssemblyInfo.cs b/Samples/ApiAsyncCachingSample/Properties/AssemblyInfo.cs deleted file mode 100644 index 577a575..0000000 --- a/Samples/ApiAsyncCachingSample/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ApiAsyncCachingSample")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ApiAsyncCachingSample")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("fb8edc6d-cd41-47aa-9758-d8d796f7728c")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Samples/ApiAsyncCachingSample/Web.Debug.config b/Samples/ApiAsyncCachingSample/Web.Debug.config deleted file mode 100644 index 24f336c..0000000 --- a/Samples/ApiAsyncCachingSample/Web.Debug.config +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Samples/ApiAsyncCachingSample/Web.Release.config b/Samples/ApiAsyncCachingSample/Web.Release.config deleted file mode 100644 index a657981..0000000 --- a/Samples/ApiAsyncCachingSample/Web.Release.config +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Samples/ApiAsyncCachingSample/Web.config b/Samples/ApiAsyncCachingSample/Web.config deleted file mode 100644 index 0598d02..0000000 --- a/Samples/ApiAsyncCachingSample/Web.config +++ /dev/null @@ -1,63 +0,0 @@ - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Samples/ApiAsyncCachingSample/packages.config b/Samples/ApiAsyncCachingSample/packages.config deleted file mode 100644 index d98fa06..0000000 --- a/Samples/ApiAsyncCachingSample/packages.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file From 7b5324b0a2c0be6395e1a8a45ddc007493db44ee Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Mon, 26 Feb 2018 20:33:12 +0000 Subject: [PATCH 04/39] task: unit tests get run in build script --- build.ps1 | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/build.ps1 b/build.ps1 index 11b6bf4..dc80a97 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,2 +1,42 @@ -dotnet restore -dotnet build -c Release \ No newline at end of file +Set-StrictMode -Version latest +$ErrorActionPreference = "Stop" + + +# Taken from psake https://github.com/psake/psake + +<# +.SYNOPSIS + This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode + to see if an error occcured. If an error is detected then an exception is thrown. + This function allows you to run command-line programs without having to + explicitly check the $lastexitcode variable. +.EXAMPLE + exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" +#> +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ("Error executing command {0}" -f $cmd) + ) + & $cmd + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } +} + +$config = "release" + +Exec { dotnet restore } + +Exec { dotnet build --configuration $config --no-restore } + +Get-ChildItem .\**\*.csproj -Recurse | Where-Object { $_.Name -match ".*Test(s)?.csproj$"} | ForEach-Object { + Exec { dotnet test $_.FullName --configuration $config --no-build --no-restore } +} + +if (Get-Command "Push-AppveyorArtifact" -errorAction SilentlyContinue) +{ + Get-ChildItem .\*\bin\$config\*.nupkg -Recurse | ForEach-Object { Push-AppveyorArtifact $_.FullName -FileName $_.Name } +} \ No newline at end of file From b4bac592e1b69392ded446df1b1fc4c5c77b2d08 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Mon, 26 Feb 2018 23:25:26 +0000 Subject: [PATCH 05/39] feat: change to cache provider model to make it possible to swap storage - also added locks on GetOrAdd and GetOrAddAsync as new dependency seems to have a race condition --- LazyCache.UnitTests/CachingServiceTests.cs | 9 +- LazyCache/CachingService.cs | 169 ++++++++++++--------- LazyCache/IAppCache.cs | 2 +- LazyCache/ICacheProvider.cs | 15 ++ LazyCache/Mocks/MockCachingService.cs | 30 +++- LazyCache/Providers/MemoryCacheProvider.cs | 44 ++++++ ReleaseNotes.md | 11 +- 7 files changed, 200 insertions(+), 80 deletions(-) create mode 100644 LazyCache/ICacheProvider.cs create mode 100644 LazyCache/Providers/MemoryCacheProvider.cs diff --git a/LazyCache.UnitTests/CachingServiceTests.cs b/LazyCache.UnitTests/CachingServiceTests.cs index c7fad3f..9dafe8d 100644 --- a/LazyCache.UnitTests/CachingServiceTests.cs +++ b/LazyCache.UnitTests/CachingServiceTests.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using LazyCache.Providers; using Microsoft.Extensions.Caching.Memory; using NUnit.Framework; @@ -11,16 +12,10 @@ namespace LazyCache.UnitTests [TestFixture] public class CachingServiceTests { - [TearDown] - public void TearDown() - { - //MemoryCache.Default.Remove(TestKey); - } - [SetUp] public void BeforeEachTest() { - sut = new CachingService(); + sut = new CachingService(new MemoryCacheProvider()); testObject = new ComplexTestObject(); } diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 84c6c40..56999fb 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -1,132 +1,172 @@ using System; +using System.Threading; using System.Threading.Tasks; +using LazyCache.Providers; using Microsoft.Extensions.Caching.Memory; namespace LazyCache { public class CachingService : IAppCache { - public CachingService() : this(new MemoryCache(new MemoryCacheOptions())) + private readonly Lazy cacheProvider; + + public static Func DefaultCacheProvider { get; set; } = () => new MemoryCacheProvider(); + + public CachingService() : this(DefaultCacheProvider) { } - public CachingService(IMemoryCache cache) + public CachingService(Func cacheProviderFactory) { - MemoryCache = cache ?? throw new ArgumentNullException(nameof(cache)); - DefaultCacheDuration = 60*20; + if (cacheProviderFactory == null) throw new ArgumentNullException(nameof(cacheProviderFactory)); + cacheProvider = new Lazy(cacheProviderFactory); + DefaultCacheDuration = 60 * 20; + } + + public CachingService(ICacheProvider cache) : this(() => cache) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); } /// /// Seconds to cache objects for by default /// - public int DefaultCacheDuration { get; set; } + public virtual int DefaultCacheDuration { get; set; } private DateTimeOffset DefaultExpiryDateTime => DateTimeOffset.Now.AddSeconds(DefaultCacheDuration); - public void Add(string key, T item) + public virtual void Add(string key, T item) { Add(key, item, DefaultExpiryDateTime); } - public void Add(string key, T item, DateTimeOffset expires) + public virtual void Add(string key, T item, DateTimeOffset expires) { Add(key, item, new MemoryCacheEntryOptions { AbsoluteExpiration = expires}); } - public void Add(string key, T item, TimeSpan slidingExpiration) + public virtual void Add(string key, T item, TimeSpan slidingExpiration) { Add(key, item, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } - public void Add(string key, T item, MemoryCacheEntryOptions policy) + public virtual void Add(string key, T item, MemoryCacheEntryOptions policy) { if (item == null) throw new ArgumentNullException(nameof(item)); ValidateKey(key); - MemoryCache.Set(key, item, policy); + CacheProvider.Set(key, item, policy); } - public T Get(string key) + public virtual T Get(string key) { ValidateKey(key); - var item = MemoryCache.Get(key); + var item = CacheProvider.Get(key); return UnwrapLazy(item); } - - public async Task GetAsync(string key) + public virtual async Task GetAsync(string key) { ValidateKey(key); - var item = MemoryCache.Get(key); + var item = CacheProvider.Get(key); return await UnwrapAsyncLazys(item); } - - public T GetOrAdd(string key, Func addItemFactory) + public virtual T GetOrAdd(string key, Func addItemFactory) { return GetOrAdd(key, addItemFactory, DefaultExpiryDateTime); } - - public async Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy) + private SemaphoreSlim locker = new SemaphoreSlim(1,1); + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy) { ValidateKey(key); EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(policy); - var cacheItem = MemoryCache.GetOrCreate(key, entry => - { - entry.SetOptions(policy); - var value = new AsyncLazy(addItemFactory); - return (object) value; - } - ); + //var cacheItem = CacheProvider.GetOrCreateAsync(key, async entry => + // await new AsyncLazy(async () => { + // entry.SetOptions(policy); + // return await addItemFactory.Invoke(); + // } + //).Value); + + //var cacheItem = CacheProvider.GetOrCreateAsync(key, async entry => + //{ + // entry.SetOptions(policy); + // return new AsyncLazy(async () => await addItemFactory.Invoke()); + //}); + + object cacheItem; + await locker.WaitAsync(); //TODO: do we really need this? + try + { + cacheItem = CacheProvider.GetOrCreate(key, entry => + { + entry.SetOptions(policy); + var value = new AsyncLazy(addItemFactory); + return (object) value; + } + ); + } + finally + { + locker.Release(); + } try { var result = UnwrapAsyncLazys(cacheItem); if (result.IsCanceled || result.IsFaulted) - MemoryCache.Remove(key); + CacheProvider.Remove(key); return await result; } catch //addItemFactory errored so do not cache the exception { - MemoryCache.Remove(key); + CacheProvider.Remove(key); throw; } } - public T GetOrAdd(string key, Func addItemFactory, DateTimeOffset expires) + public virtual T GetOrAdd(string key, Func addItemFactory, DateTimeOffset expires) { return GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); } - - public T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpiration) + public virtual T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpiration) { return GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } - public T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy) + public virtual T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy) { ValidateKey(key); EnsureRemovedCallbackDoesNotReturnTheLazy(policy); - var cacheItem = MemoryCache.GetOrCreate(key, entry => - { - entry.SetOptions(policy); - var value = new Lazy(addItemFactory); - return (object)value; - } - ); + object cacheItem; + locker.Wait(); //TODO: do we really need this? + try + { + cacheItem = CacheProvider.GetOrCreate(key, entry => + { + entry.SetOptions(policy); + var value = new Lazy(addItemFactory); + return (object) value; + } + ); + } + finally + { + locker.Release(); + } try { @@ -134,76 +174,69 @@ public T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions } catch //addItemFactory errored so do not cache the exception { - MemoryCache.Remove(key); + CacheProvider.Remove(key); throw; } } - - public void Remove(string key) + public virtual void Remove(string key) { ValidateKey(key); - MemoryCache.Remove(key); + CacheProvider.Remove(key); } - public IMemoryCache MemoryCache { get; } + public virtual ICacheProvider CacheProvider => cacheProvider.Value; - public async Task GetOrAddAsync(string key, Func> addItemFactory) + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory) { return await GetOrAddAsync(key, addItemFactory, DefaultExpiryDateTime); } - public async Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires) + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires) { return await GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); } - public async Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration) + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration) { return await GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } - private static T UnwrapLazy(object item) + protected virtual T UnwrapLazy(object item) { - var lazy = item as Lazy; - if (lazy != null) + if (item is Lazy lazy) return lazy.Value; - if (item is T) - return (T) item; + if (item is T variable) + return variable; - var asyncLazy = item as AsyncLazy; - if (asyncLazy != null) + if (item is AsyncLazy asyncLazy) return asyncLazy.Value.ConfigureAwait(false).GetAwaiter().GetResult(); - var task = item as Task; - if (task != null) + if (item is Task task) return task.Result; return default(T); } - private static async Task UnwrapAsyncLazys(object item) + protected virtual async Task UnwrapAsyncLazys(object item) { - var asyncLazy = item as AsyncLazy; - if (asyncLazy != null) + if (item is AsyncLazy asyncLazy) return await asyncLazy.Value; - var task = item as Task; - if (task != null) + if (item is Task task) return await task; - var lazy = item as Lazy; - if (lazy != null) + if (item is Lazy lazy) return lazy.Value; - if (item is T) - return (T) item; + if (item is T variable) + return variable; return default(T); } - private static void EnsureRemovedCallbackDoesNotReturnTheLazy(MemoryCacheEntryOptions policy) + protected virtual void EnsureRemovedCallbackDoesNotReturnTheLazy(MemoryCacheEntryOptions policy) { if (policy?.PostEvictionCallbacks != null) { @@ -221,7 +254,7 @@ private static void EnsureRemovedCallbackDoesNotReturnTheLazy(MemoryCacheEntr } } - private static void EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(MemoryCacheEntryOptions policy) + protected virtual void EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(MemoryCacheEntryOptions policy) { if (policy?.PostEvictionCallbacks != null) foreach (var item in policy.PostEvictionCallbacks) @@ -239,7 +272,7 @@ private static void EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(MemoryCach } } - private void ValidateKey(string key) + protected virtual void ValidateKey(string key) { if (key == null) throw new ArgumentNullException(nameof(key)); diff --git a/LazyCache/IAppCache.cs b/LazyCache/IAppCache.cs index c4cc1f1..1128918 100644 --- a/LazyCache/IAppCache.cs +++ b/LazyCache/IAppCache.cs @@ -6,7 +6,7 @@ namespace LazyCache { public interface IAppCache { - IMemoryCache MemoryCache { get; } + ICacheProvider CacheProvider { get; } void Add(string key, T item); void Add(string key, T item, DateTimeOffset absoluteExpiration); void Add(string key, T item, TimeSpan slidingExpiration); diff --git a/LazyCache/ICacheProvider.cs b/LazyCache/ICacheProvider.cs new file mode 100644 index 0000000..9b73220 --- /dev/null +++ b/LazyCache/ICacheProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; + +namespace LazyCache +{ + public interface ICacheProvider + { + void Set(string key, object item, MemoryCacheEntryOptions policy); + object Get(string key); + object GetOrCreate(string key, Func func); + void Remove(string key); + Task GetOrCreateAsync(string key, Func> func); + } +} \ No newline at end of file diff --git a/LazyCache/Mocks/MockCachingService.cs b/LazyCache/Mocks/MockCachingService.cs index f547c86..4b12395 100644 --- a/LazyCache/Mocks/MockCachingService.cs +++ b/LazyCache/Mocks/MockCachingService.cs @@ -10,6 +10,8 @@ namespace LazyCache.Mocks /// public class MockCachingService : IAppCache { + public ICacheProvider CacheProvider { get; } = new MockCacheProvider(); + public void Add(string key, T item) { } @@ -62,8 +64,6 @@ public Task GetAsync(string key) return Task.FromResult(default(T)); } - public IMemoryCache MemoryCache => null; - public void Add(string key, T item, TimeSpan slidingExpiration) { @@ -83,4 +83,30 @@ public T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions return addItemFactory.Invoke(); } } + + public class MockCacheProvider : ICacheProvider + { + public void Set(string key, object item, MemoryCacheEntryOptions policy) + { + } + + public object Get(string key) + { + return null; + } + + public object GetOrCreate(string key, Func func) + { + return func(null); + } + + public void Remove(string key) + { + } + + public Task GetOrCreateAsync(string key, Func> func) + { + return func(null); + } + } } \ No newline at end of file diff --git a/LazyCache/Providers/MemoryCacheProvider.cs b/LazyCache/Providers/MemoryCacheProvider.cs new file mode 100644 index 0000000..4d6aaf4 --- /dev/null +++ b/LazyCache/Providers/MemoryCacheProvider.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; + +namespace LazyCache.Providers +{ + public class MemoryCacheProvider : ICacheProvider + { + public MemoryCacheProvider() : this(new MemoryCache(new MemoryCacheOptions())) + { + + } + private readonly IMemoryCache cache; + + public MemoryCacheProvider(IMemoryCache cache) + { + this.cache = cache; + } + public void Set(string key, object item, MemoryCacheEntryOptions policy) + { + cache.Set(key, item, policy); + } + + public object Get(string key) + { + return cache.Get(key); + } + + public object GetOrCreate(string key, Func factory) + { + return cache.GetOrCreate(key, factory); + } + + public void Remove(string key) + { + cache.Remove(key); + } + + public Task GetOrCreateAsync(string key, Func> factory) + { + return cache.GetOrCreateAsync(key, factory); + } + } +} \ No newline at end of file diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 1134385..d32c759 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -3,8 +3,15 @@ ## Version 2.0.0 - *BREAKING CHANGE* Upgrade to netstandard2.0 - *BREAKING CHANGE* Change underlying cache from System.Runtime.Caching to Microsft.Extension.Caching.Memory -- *BREAKING CHANGE* Removed IAppCache.ObjectCache (change to IAppCache.MemoryCache) -- *BREAKING CHANGE* changed from CacheItemPolicy to MemoryCacheEntryOptions. Now uses PostEvictionCallbacks. +- *BREAKING CHANGE* Removed IAppCache.ObjectCache and changed to a cache provider model. + To access the provider use IAppCache.CacheProvider. By default we use a static shared in-memory cache but add your own cache provider by implmenting the simple `ICacheProvider`. +- *BREAKING CHANGE* changed from CacheItemPolicy to MemoryCacheEntryOptions. RemovedCallback is now PostEvictionCallbacks. +- Added a new replaceable global static default cache provider + + `Func DefaultCacheProvider { get; }` + + By default we use a shared in-memory cache but each instance can have it's underlying cache provider overridden from it's constructor. +- Make methods on CachingService virtual ## Version 0.7.1 - Fix async/sync interopability bug, see https://github.com/alastairtree/LazyCache/issues/12 From 7fc5eae64bc84f0bb8fccb08ce8d83c1cfefbb4c Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Mon, 26 Feb 2018 23:38:23 +0000 Subject: [PATCH 06/39] chore: errors should fail the build --- build.ps1 | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/build.ps1 b/build.ps1 index dc80a97..bdc807e 100644 --- a/build.ps1 +++ b/build.ps1 @@ -28,15 +28,20 @@ function Exec $config = "release" -Exec { dotnet restore } - -Exec { dotnet build --configuration $config --no-restore } - -Get-ChildItem .\**\*.csproj -Recurse | Where-Object { $_.Name -match ".*Test(s)?.csproj$"} | ForEach-Object { - Exec { dotnet test $_.FullName --configuration $config --no-build --no-restore } -} - -if (Get-Command "Push-AppveyorArtifact" -errorAction SilentlyContinue) -{ - Get-ChildItem .\*\bin\$config\*.nupkg -Recurse | ForEach-Object { Push-AppveyorArtifact $_.FullName -FileName $_.Name } +Try { + Exec { dotnet restore } + + Exec { dotnet build --configuration $config --no-restore } + + Get-ChildItem .\**\*.csproj -Recurse | Where-Object { $_.Name -match ".*Test(s)?.csproj$"} | ForEach-Object { + Exec { dotnet test $_.FullName --configuration $config --no-build --no-restore } + } + + if (Get-Command "Push-AppveyorArtifact" -errorAction SilentlyContinue) + { + Get-ChildItem .\*\bin\$config\*.nupkg -Recurse | ForEach-Object { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + } +} Catch { + $host.SetShouldExit(-1) + throw } \ No newline at end of file From 2a46e53cd890977592d476823c81469552226daf Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Mon, 26 Feb 2018 23:41:57 +0000 Subject: [PATCH 07/39] chore: fix appveyor.yml --- appveyor.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 590c30b..ae170ee 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,11 +1,5 @@ version: 2.0.0.{build}-beta01 configuration: Release -assembly_info: - patch: true - file: '**\AssemblyInfo.*' - assembly_version: '{version}' - assembly_file_version: '{version}' - assembly_informational_version: '{version}' dotnet_csproj: patch: true file: '**\*.csproj' From 55d383bfea8ef1558b65ae5b28eeb8ebbf88a2ea Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Tue, 27 Feb 2018 22:27:05 +0000 Subject: [PATCH 08/39] test: send unit tests results to appveyor --- build.ps1 | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/build.ps1 b/build.ps1 index bdc807e..6e01ea6 100644 --- a/build.ps1 +++ b/build.ps1 @@ -29,18 +29,33 @@ function Exec $config = "release" Try { - Exec { dotnet restore } + # Get dependencies from nuget and compile + Exec { dotnet restore } Exec { dotnet build --configuration $config --no-restore } - Get-ChildItem .\**\*.csproj -Recurse | Where-Object { $_.Name -match ".*Test(s)?.csproj$"} | ForEach-Object { - Exec { dotnet test $_.FullName --configuration $config --no-build --no-restore } + # Find each test project and run tests. upload results to AppVeyor + Get-ChildItem .\**\*.csproj -Recurse | + Where-Object { $_.Name -match ".*Test(s)?.csproj$"} | + ForEach-Object { + + Exec { dotnet test $_.FullName --configuration $config --no-build --no-restore --logger "trx;LogFileName=..\..\test-result.trx" } + + # if on build server upload results to AppVeyor + if ("${ENV:APPVEYOR_JOB_ID}" -ne "") { + $wc = New-Object 'System.Net.WebClient' + $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\test-result.trx)) + } + + Remove-Item .\test-result.trx -ErrorAction SilentlyContinue } + # Publish the nupkg artifacts if (Get-Command "Push-AppveyorArtifact" -errorAction SilentlyContinue) { Get-ChildItem .\*\bin\$config\*.nupkg -Recurse | ForEach-Object { Push-AppveyorArtifact $_.FullName -FileName $_.Name } } + } Catch { $host.SetShouldExit(-1) throw From b2041674b2b793a5bb8a60a0a6b9f2adc12138ce Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 16:43:36 +0000 Subject: [PATCH 09/39] chore: code clean --- LazyCache.UnitTests/AsyncHelper.cs | 5 +- LazyCache.UnitTests/CachingServiceTests.cs | 199 +++++++++++---------- LazyCache/AsyncLazy.cs | 6 +- LazyCache/CachingService.cs | 25 +-- LazyCache/Providers/MemoryCacheProvider.cs | 9 +- 5 files changed, 126 insertions(+), 118 deletions(-) diff --git a/LazyCache.UnitTests/AsyncHelper.cs b/LazyCache.UnitTests/AsyncHelper.cs index 89a866c..9f82c7a 100644 --- a/LazyCache.UnitTests/AsyncHelper.cs +++ b/LazyCache.UnitTests/AsyncHelper.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace LazyCache.UnitTests @@ -22,4 +19,4 @@ public static Task CreateTaskWithException() where TException return tcs.Task; } } -} +} \ No newline at end of file diff --git a/LazyCache.UnitTests/CachingServiceTests.cs b/LazyCache.UnitTests/CachingServiceTests.cs index 9dafe8d..d47d0f9 100644 --- a/LazyCache.UnitTests/CachingServiceTests.cs +++ b/LazyCache.UnitTests/CachingServiceTests.cs @@ -21,11 +21,12 @@ public void BeforeEachTest() private CachingService sut; - private readonly MemoryCacheEntryOptions oneHourNonRemoveableMemoryCacheEntryOptions = new MemoryCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddHours(1), - Priority = CacheItemPriority.NeverRemove - }; + private readonly MemoryCacheEntryOptions oneHourNonRemoveableMemoryCacheEntryOptions = + new MemoryCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddHours(1), + Priority = CacheItemPriority.NeverRemove + }; private ComplexTestObject testObject = new ComplexTestObject(); @@ -216,49 +217,67 @@ public void GetNullKeyThrowsException() } [Test] - public async Task GetOrAddFollowinGetOrAddAsyncReturnsTheFirstObjectAndUnwrapsTheFirstTask() + public void GetOrAddAndThenGetObjectReturnsCorrectType() { - Func> fetchAsync = () => Task.FromResult(testObject); - Func fetchSync = () => new ComplexTestObject(); - - var actualAsync = await sut.GetOrAddAsync(TestKey, fetchAsync); - var actualSync = sut.GetOrAdd(TestKey, fetchSync); - - Assert.IsNotNull(actualAsync); - Assert.That(actualAsync, Is.EqualTo(testObject)); + Func fetch = () => testObject; + sut.GetOrAdd(TestKey, fetch); + var actual = sut.Get(TestKey); + Assert.IsNotNull(actual); + } - Assert.IsNotNull(actualSync); - Assert.That(actualSync, Is.EqualTo(testObject)); + [Test] + public void GetOrAddAndThenGetValueObjectReturnsCorrectType() + { + Func fetch = () => 123; + sut.GetOrAdd(TestKey, fetch); + var actual = sut.Get(TestKey); + Assert.AreEqual(123, actual); + } - Assert.AreEqual(actualAsync, actualSync); + [Test] + public void GetOrAddAndThenGetWrongtypeObjectReturnsNull() + { + Func fetch = () => testObject; + sut.GetOrAdd(TestKey, fetch); + var actual = sut.Get(TestKey); + Assert.IsNull(actual); } [Test] - public async Task GetOrAddAsyncFollowinGetOrAddReturnsTheFirstObjectAndIgnoresTheSecondTask() + public void GetOrAddAsyncACancelledTaskDoesNotCacheIt() { - Func> fetchAsync = () => Task.FromResult(new ComplexTestObject()); - Func fetchSync = () => testObject; + Assert.ThrowsAsync(async () => + await sut.GetOrAddAsync(TestKey, AsyncHelper.CreateCancelledTask)); - var actualSync = sut.GetOrAdd(TestKey, fetchSync); - var actualAsync = await sut.GetOrAddAsync(TestKey, fetchAsync); + var stillCached = sut.Get>(TestKey); + Assert.That(stillCached, Is.Null); + } - Assert.IsNotNull(actualSync); - Assert.That(actualSync, Is.EqualTo(testObject)); + [Test] + public void GetOrAddAsyncACancelledTaskReturnsTheCacelledTaskToConsumer() + { + var cancelledTask = sut.GetOrAddAsync(TestKey, AsyncHelper.CreateCancelledTask); - Assert.IsNotNull(actualAsync); - Assert.That(actualAsync, Is.EqualTo(testObject)); + Assert.That(cancelledTask, Is.Not.Null); - Assert.AreEqual(actualAsync, actualSync); + Assert.Throws(cancelledTask.Wait); + + Assert.That(cancelledTask.IsCanceled, Is.True); } [Test] - public void GetOrAddAndThenGetObjectReturnsCorrectType() + public void GetOrAddAsyncAFailingTaskDoesNotCacheIt() { - Func fetch = () => testObject; - sut.GetOrAdd(TestKey, fetch); - var actual = sut.Get(TestKey); - Assert.IsNotNull(actual); + Func> fetchAsync = () => + Task.Factory.StartNew( + () => { throw new ApplicationException(); }); + + Assert.ThrowsAsync(async () => await sut.GetOrAddAsync(TestKey, fetchAsync)); + + var stillCached = sut.Get>(TestKey); + + Assert.That(stillCached, Is.Null); } [Test] @@ -281,21 +300,22 @@ public async Task GetOrAddAsyncAndThenGetAsyncWrongObjectReturnsNull() } [Test] - public void GetOrAddAndThenGetValueObjectReturnsCorrectType() + public async Task GetOrAddAsyncFollowinGetOrAddReturnsTheFirstObjectAndIgnoresTheSecondTask() { - Func fetch = () => 123; - sut.GetOrAdd(TestKey, fetch); - var actual = sut.Get(TestKey); - Assert.AreEqual(123, actual); - } + Func> fetchAsync = () => Task.FromResult(new ComplexTestObject()); + Func fetchSync = () => testObject; - [Test] - public void GetOrAddAndThenGetWrongtypeObjectReturnsNull() - { - Func fetch = () => testObject; - sut.GetOrAdd(TestKey, fetch); - var actual = sut.Get(TestKey); - Assert.IsNull(actual); + var actualSync = sut.GetOrAdd(TestKey, fetchSync); + var actualAsync = await sut.GetOrAddAsync(TestKey, fetchAsync); + + + Assert.IsNotNull(actualSync); + Assert.That(actualSync, Is.EqualTo(testObject)); + + Assert.IsNotNull(actualAsync); + Assert.That(actualAsync, Is.EqualTo(testObject)); + + Assert.AreEqual(actualAsync, actualSync); } [Test] @@ -373,6 +393,21 @@ public async Task GetOrAddAsyncWillNotAddIfExistingData() Assert.AreEqual(0, times); } + [Test] + [MaxTime(1000)] + public void GetOrAddAsyncWithALongTaskReturnsBeforeTaskCompletes() + { + var cachedResult = testObject; + Func> fetchAsync = () => + Task.Delay(TimeSpan.FromMinutes(1)).ContinueWith(x => cachedResult); + + var actualResult = sut.GetOrAddAsync(TestKey, fetchAsync); + + Assert.That(actualResult, Is.Not.Null); + Assert.That(actualResult.IsCompleted, Is.Not.True); + } + + // TODO: restore this test //[Test, MaxTime(20000)] //public async Task GetOrAddAsyncWithCallbackOnRemovedReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() //{ @@ -404,7 +439,7 @@ public async Task GetOrAddAsyncWithOffsetWillAddAndReturnTaskOfCached() TestKey, () => Task.FromResult(new DateTime(2001, 01, 01)), DateTimeOffset.Now.AddSeconds(5) - ); + ); var expectedSecond = await sut.Get>(TestKey); Assert.AreEqual(2001, expectedFirst.Year); @@ -428,11 +463,30 @@ public async Task GetOrAddAyncAllowsCachingATask() var cachedResult = testObject; Func> fetchAsync = () => Task.FromResult(cachedResult); - var actualResult = await sut.GetOrAddAsync(TestKey, fetchAsync, oneHourNonRemoveableMemoryCacheEntryOptions); + var actualResult = + await sut.GetOrAddAsync(TestKey, fetchAsync, oneHourNonRemoveableMemoryCacheEntryOptions); Assert.That(actualResult, Is.EqualTo(cachedResult)); } + [Test] + public async Task GetOrAddFollowinGetOrAddAsyncReturnsTheFirstObjectAndUnwrapsTheFirstTask() + { + Func> fetchAsync = () => Task.FromResult(testObject); + Func fetchSync = () => new ComplexTestObject(); + + var actualAsync = await sut.GetOrAddAsync(TestKey, fetchAsync); + var actualSync = sut.GetOrAdd(TestKey, fetchSync); + + Assert.IsNotNull(actualAsync); + Assert.That(actualAsync, Is.EqualTo(testObject)); + + Assert.IsNotNull(actualSync); + Assert.That(actualSync, Is.EqualTo(testObject)); + + Assert.AreEqual(actualAsync, actualSync); + } + [Test] public void GetOrAddWillAddOnFirstCall() { @@ -515,7 +569,7 @@ public void GetOrAddWithOffsetWillAddAndReturnCached() TestKey, () => new DateTime(2001, 01, 01), DateTimeOffset.Now.AddSeconds(5) - ); + ); var expectedSecond = sut.Get(TestKey); Assert.AreEqual(2001, expectedFirst.Year); @@ -657,54 +711,5 @@ public void RemovedItemCannotBeRetrievedFromCache() sut.Remove(TestKey); Assert.Null(sut.Get(TestKey)); } - - [Test] - [MaxTime(1000)] - public void GetOrAddAsyncWithALongTaskReturnsBeforeTaskCompletes() - { - var cachedResult = testObject; - Func> fetchAsync = () => Task.Delay(TimeSpan.FromMinutes(1)).ContinueWith(x=> cachedResult); - - var actualResult = sut.GetOrAddAsync(TestKey, fetchAsync); - - Assert.That(actualResult, Is.Not.Null); - Assert.That(actualResult.IsCompleted, Is.Not.True); - } - - [Test] - public void GetOrAddAsyncAFailingTaskDoesNotCacheIt() - { - Func> fetchAsync = () => - Task.Factory.StartNew( - () => { throw new ApplicationException(); }); - - Assert.ThrowsAsync(async () => await sut.GetOrAddAsync(TestKey, fetchAsync)); - - var stillCached = sut.Get>(TestKey); - - Assert.That(stillCached, Is.Null); - } - - [Test] - public void GetOrAddAsyncACancelledTaskDoesNotCacheIt() - { - Assert.ThrowsAsync(async () => await sut.GetOrAddAsync(TestKey, AsyncHelper.CreateCancelledTask)); - - var stillCached = sut.Get>(TestKey); - - Assert.That(stillCached, Is.Null); - } - - [Test] - public void GetOrAddAsyncACancelledTaskReturnsTheCacelledTaskToConsumer() - { - var cancelledTask = sut.GetOrAddAsync(TestKey, AsyncHelper.CreateCancelledTask); - - Assert.That(cancelledTask, Is.Not.Null); - - Assert.Throws(cancelledTask.Wait); - - Assert.That(cancelledTask.IsCanceled, Is.True); - } } } \ No newline at end of file diff --git a/LazyCache/AsyncLazy.cs b/LazyCache/AsyncLazy.cs index b52c837..769d6bd 100644 --- a/LazyCache/AsyncLazy.cs +++ b/LazyCache/AsyncLazy.cs @@ -19,6 +19,10 @@ public AsyncLazy(Func> taskFactory) : base(() => Task.Factory.StartNew(taskFactory).Unwrap()) { } - public TaskAwaiter GetAwaiter() { return Value.GetAwaiter(); } + + public TaskAwaiter GetAwaiter() + { + return Value.GetAwaiter(); + } } } \ No newline at end of file diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 56999fb..56a30d4 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -10,7 +10,7 @@ public class CachingService : IAppCache { private readonly Lazy cacheProvider; - public static Func DefaultCacheProvider { get; set; } = () => new MemoryCacheProvider(); + private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); public CachingService() : this(DefaultCacheProvider) { @@ -28,8 +28,10 @@ public CachingService(ICacheProvider cache) : this(() => cache) if (cache == null) throw new ArgumentNullException(nameof(cache)); } + public static Func DefaultCacheProvider { get; set; } = () => new MemoryCacheProvider(); + /// - /// Seconds to cache objects for by default + /// Seconds to cache objects for by default /// public virtual int DefaultCacheDuration { get; set; } @@ -42,7 +44,7 @@ public virtual void Add(string key, T item) public virtual void Add(string key, T item, DateTimeOffset expires) { - Add(key, item, new MemoryCacheEntryOptions { AbsoluteExpiration = expires}); + Add(key, item, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); } public virtual void Add(string key, T item, TimeSpan slidingExpiration) @@ -82,8 +84,8 @@ public virtual T GetOrAdd(string key, Func addItemFactory) return GetOrAdd(key, addItemFactory, DefaultExpiryDateTime); } - private SemaphoreSlim locker = new SemaphoreSlim(1,1); - public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy) + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, + MemoryCacheEntryOptions policy) { ValidateKey(key); @@ -197,9 +199,11 @@ public virtual async Task GetOrAddAsync(string key, Func> addItemF return await GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); } - public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration) + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, + TimeSpan slidingExpiration) { - return await GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); + return await GetOrAddAsync(key, addItemFactory, + new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } protected virtual T UnwrapLazy(object item) @@ -239,7 +243,6 @@ protected virtual async Task UnwrapAsyncLazys(object item) protected virtual void EnsureRemovedCallbackDoesNotReturnTheLazy(MemoryCacheEntryOptions policy) { if (policy?.PostEvictionCallbacks != null) - { foreach (var item in policy.PostEvictionCallbacks) { var originallCallback = item.EvictionCallback; @@ -247,11 +250,11 @@ protected virtual void EnsureRemovedCallbackDoesNotReturnTheLazy(MemoryCacheE { //unwrap the cache item in a callback given one is specified if (value is Lazy cacheItem) - value = cacheItem.IsValueCreated ? cacheItem.Value : default(T); ; + value = cacheItem.IsValueCreated ? cacheItem.Value : default(T); + ; originallCallback(key, value, reason, state); }; } - } } protected virtual void EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(MemoryCacheEntryOptions policy) @@ -264,9 +267,7 @@ protected virtual void EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(MemoryC { //unwrap the cache item in a callback given one is specified if (value is AsyncLazy cacheItem) - { value = cacheItem.IsValueCreated ? cacheItem.Value : Task.FromResult(default(T)); - } originalCallback(key, value, reason, state); }; } diff --git a/LazyCache/Providers/MemoryCacheProvider.cs b/LazyCache/Providers/MemoryCacheProvider.cs index 4d6aaf4..c85f116 100644 --- a/LazyCache/Providers/MemoryCacheProvider.cs +++ b/LazyCache/Providers/MemoryCacheProvider.cs @@ -6,16 +6,17 @@ namespace LazyCache.Providers { public class MemoryCacheProvider : ICacheProvider { + private readonly IMemoryCache cache; + public MemoryCacheProvider() : this(new MemoryCache(new MemoryCacheOptions())) { - } - private readonly IMemoryCache cache; public MemoryCacheProvider(IMemoryCache cache) { this.cache = cache; } + public void Set(string key, object item, MemoryCacheEntryOptions policy) { cache.Set(key, item, policy); @@ -28,7 +29,7 @@ public object Get(string key) public object GetOrCreate(string key, Func factory) { - return cache.GetOrCreate(key, factory); + return cache.GetOrCreate(key, factory); } public void Remove(string key) @@ -38,7 +39,7 @@ public void Remove(string key) public Task GetOrCreateAsync(string key, Func> factory) { - return cache.GetOrCreateAsync(key, factory); + return cache.GetOrCreateAsync(key, factory); } } } \ No newline at end of file From 8260013928c62687ddde2b2dca3543aa55875c41 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 17:54:36 +0000 Subject: [PATCH 10/39] bug: fix default constructor not using a shared cache --- ...chingService_MemoryCacheProvider_Tests.cs} | 28 ++++++++++++++----- LazyCache/CachingService.cs | 27 ++++++++++++++---- 2 files changed, 43 insertions(+), 12 deletions(-) rename LazyCache.UnitTests/{CachingServiceTests.cs => CachingService_MemoryCacheProvider_Tests.cs} (93%) diff --git a/LazyCache.UnitTests/CachingServiceTests.cs b/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs similarity index 93% rename from LazyCache.UnitTests/CachingServiceTests.cs rename to LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs index d47d0f9..3cffdfb 100644 --- a/LazyCache.UnitTests/CachingServiceTests.cs +++ b/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs @@ -10,15 +10,20 @@ namespace LazyCache.UnitTests { [TestFixture] - public class CachingServiceTests + public class CachingService_MemoryCacheProvider_Tests { [SetUp] public void BeforeEachTest() { - sut = new CachingService(new MemoryCacheProvider()); + sut = BuildCache(); testObject = new ComplexTestObject(); } + private static CachingService BuildCache() + { + return new CachingService(new MemoryCacheProvider()); + } + private CachingService sut; private readonly MemoryCacheEntryOptions oneHourNonRemoveableMemoryCacheEntryOptions = @@ -197,11 +202,7 @@ public void GetFromCacheTwiceAtSameTimeOnlyAddsOnce() var t2 = Task.Factory.StartNew(() => { - sut.GetOrAdd(TestKey, () => - { - Interlocked.Increment(ref times); - return new DateTime(2001, 01, 01); - }); + sut.GetOrAdd(TestKey, () => throw new Exception("Should not be called!")); }); Task.WaitAll(t1, t2); @@ -216,6 +217,19 @@ public void GetNullKeyThrowsException() act.Should().Throw(); } + [Test] + public void DefaultContructorThenGetOrAddFromSecondCachingServiceHasSharedUnderlyingCache() + { + var cacheOne = new CachingService(); + var cacheTwo = new CachingService(); ; // second instance + + var resultOne = cacheOne.GetOrAdd(TestKey, () => "resultOne"); + var resultTwo = cacheTwo.GetOrAdd(TestKey, () => "resultTwo"); // should not get executed + + resultOne.Should().Be("resultOne", "GetOrAdd should execute the delegate"); + resultTwo.Should().Be("resultOne", "CachingService should use a shared cache by default"); + } + [Test] public void GetOrAddAndThenGetObjectReturnsCorrectType() { diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 56a30d4..76f3481 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -16,11 +16,16 @@ public CachingService() : this(DefaultCacheProvider) { } + public CachingService(Lazy cacheProvider) + { + this.cacheProvider = cacheProvider ?? throw new ArgumentNullException(nameof(cacheProvider)); + } + + public CachingService(Func cacheProviderFactory) { if (cacheProviderFactory == null) throw new ArgumentNullException(nameof(cacheProviderFactory)); cacheProvider = new Lazy(cacheProviderFactory); - DefaultCacheDuration = 60 * 20; } public CachingService(ICacheProvider cache) : this(() => cache) @@ -28,14 +33,26 @@ public CachingService(ICacheProvider cache) : this(() => cache) if (cache == null) throw new ArgumentNullException(nameof(cache)); } - public static Func DefaultCacheProvider { get; set; } = () => new MemoryCacheProvider(); + public static Lazy DefaultCacheProvider { get; set; } + = new Lazy(() => new MemoryCacheProvider()); + + /// + /// Seconds to cache objects for by default + /// + public virtual int DefaultCacheDurationSeconds { get; set; } = 60 * 20; /// - /// Seconds to cache objects for by default + /// Seconds to cache objects for by default /// - public virtual int DefaultCacheDuration { get; set; } + [Obsolete("DefaultCacheDuration has been replaced with DefaultCacheDurationSeconds")] + + public virtual int DefaultCacheDuration + { + get => DefaultCacheDurationSeconds; + set => DefaultCacheDurationSeconds = value; + } - private DateTimeOffset DefaultExpiryDateTime => DateTimeOffset.Now.AddSeconds(DefaultCacheDuration); + private DateTimeOffset DefaultExpiryDateTime => DateTimeOffset.Now.AddSeconds(DefaultCacheDurationSeconds); public virtual void Add(string key, T item) { From 5b65ea08e15aadaa91e46d93fa56f3ec52a191ce Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 18:04:24 +0000 Subject: [PATCH 11/39] task: tidy up the sample app & fix default file loading --- .../Controllers/DbTimeController.cs | 7 +++--- CacheDatabaseQueriesApiSample/DbTime.cs | 23 ------------------- .../DbTimeContext.cs | 8 ++++--- CacheDatabaseQueriesApiSample/DbTimeEntity.cs | 23 +++++++++++++++++++ CacheDatabaseQueriesApiSample/Program.cs | 17 +++++--------- .../Properties/launchSettings.json | 4 ++-- CacheDatabaseQueriesApiSample/Startup.cs | 17 ++++---------- .../wwwroot/index.html | 2 +- 8 files changed, 45 insertions(+), 56 deletions(-) delete mode 100644 CacheDatabaseQueriesApiSample/DbTime.cs create mode 100644 CacheDatabaseQueriesApiSample/DbTimeEntity.cs diff --git a/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs b/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs index 5d2aca5..572e90f 100644 --- a/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs +++ b/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using LazyCache; using Microsoft.AspNetCore.Mvc; @@ -21,11 +20,11 @@ public DbTimeController(DbTimeContext context) [HttpGet] [Route("api/dbtime")] - public DbTime Get() + public DbTimeEntity Get() { - Func cacheableAsyncFunc = () => dbContext.GeDbTime(); + Func actionThatWeWantToCache = () => dbContext.GeDbTime(); - var cachedDatabaseTime = cache.GetOrAdd(cacheKey, cacheableAsyncFunc); + var cachedDatabaseTime = cache.GetOrAdd(cacheKey, actionThatWeWantToCache); return cachedDatabaseTime; } diff --git a/CacheDatabaseQueriesApiSample/DbTime.cs b/CacheDatabaseQueriesApiSample/DbTime.cs deleted file mode 100644 index 253cd92..0000000 --- a/CacheDatabaseQueriesApiSample/DbTime.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace CacheDatabaseQueriesApiSample -{ - public class DbTime - { - public DbTime(DateTime now) - { - TimeNowInTheDatabase = now; - } - - public DbTime() - { - - } - - public virtual int id { get; set; } - - public virtual DateTime TimeNowInTheDatabase { get; set; } - - - } -} \ No newline at end of file diff --git a/CacheDatabaseQueriesApiSample/DbTimeContext.cs b/CacheDatabaseQueriesApiSample/DbTimeContext.cs index 6147748..0990351 100644 --- a/CacheDatabaseQueriesApiSample/DbTimeContext.cs +++ b/CacheDatabaseQueriesApiSample/DbTimeContext.cs @@ -12,17 +12,19 @@ public DbTimeContext(DbContextOptions options) { } - public virtual DbSet Times { get; set; } + // simulate a table in the database so we can get just one row with the current time + private DbSet Times { get; set; } public static int DatabaseRequestCounter() { return databaseRequestCounter; } - public DbTime GeDbTime() + public DbTimeEntity GeDbTime() { // get the current time from SQL server right now asynchronously (simulating a slow query) - var result = Times.FromSql("WAITFOR DELAY '00:00:00:500'; SELECT 1 as ID, GETDATE() as [TimeNowInTheDatabase]") + var result = Times + .FromSql("WAITFOR DELAY '00:00:00:500'; SELECT 1 as [ID], GETDATE() as [TimeNowInTheDatabase]") .Single(); databaseRequestCounter++; diff --git a/CacheDatabaseQueriesApiSample/DbTimeEntity.cs b/CacheDatabaseQueriesApiSample/DbTimeEntity.cs new file mode 100644 index 0000000..b40f609 --- /dev/null +++ b/CacheDatabaseQueriesApiSample/DbTimeEntity.cs @@ -0,0 +1,23 @@ +using System; + +namespace CacheDatabaseQueriesApiSample +{ + /// + /// Simulates loading a record from a table, but really just gets the current datatime from the database + /// + public class DbTimeEntity + { + public DbTimeEntity(DateTime now) + { + TimeNowInTheDatabase = now; + } + + public DbTimeEntity() + { + } + + public virtual int id { get; set; } + + public virtual DateTime TimeNowInTheDatabase { get; set; } + } +} \ No newline at end of file diff --git a/CacheDatabaseQueriesApiSample/Program.cs b/CacheDatabaseQueriesApiSample/Program.cs index 8309742..1807f13 100644 --- a/CacheDatabaseQueriesApiSample/Program.cs +++ b/CacheDatabaseQueriesApiSample/Program.cs @@ -1,12 +1,5 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; namespace CacheDatabaseQueriesApiSample { @@ -17,9 +10,11 @@ public static void Main(string[] args) BuildWebHost(args).Run(); } - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) + public static IWebHost BuildWebHost(string[] args) + { + return WebHost.CreateDefaultBuilder(args) .UseStartup() .Build(); + } } -} +} \ No newline at end of file diff --git a/CacheDatabaseQueriesApiSample/Properties/launchSettings.json b/CacheDatabaseQueriesApiSample/Properties/launchSettings.json index d88345d..47fffba 100644 --- a/CacheDatabaseQueriesApiSample/Properties/launchSettings.json +++ b/CacheDatabaseQueriesApiSample/Properties/launchSettings.json @@ -11,7 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "index.html", + "launchUrl": "/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -19,7 +19,7 @@ "CacheDatabaseQueriesApiSample": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "index.html", + "launchUrl": "/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/CacheDatabaseQueriesApiSample/Startup.cs b/CacheDatabaseQueriesApiSample/Startup.cs index 48ec8cc..8af2581 100644 --- a/CacheDatabaseQueriesApiSample/Startup.cs +++ b/CacheDatabaseQueriesApiSample/Startup.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace CacheDatabaseQueriesApiSample { @@ -25,7 +19,8 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddMvc(); - var connection = @"Server=(localdb)\projectsv13;Database=Master;Trusted_Connection=True;ConnectRetryCount=0"; + var connection = + @"Server=(localdb)\projectsv13;Database=Master;Trusted_Connection=True;ConnectRetryCount=0"; services.AddDbContext(options => options.UseSqlServer(connection)); } @@ -33,13 +28,11 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) - { app.UseDeveloperExceptionPage(); - } - app.UseStaticFiles(); app.UseDefaultFiles(); + app.UseStaticFiles(); app.UseMvc(); } } -} +} \ No newline at end of file diff --git a/CacheDatabaseQueriesApiSample/wwwroot/index.html b/CacheDatabaseQueriesApiSample/wwwroot/index.html index 1a480ef..dd9faa3 100644 --- a/CacheDatabaseQueriesApiSample/wwwroot/index.html +++ b/CacheDatabaseQueriesApiSample/wwwroot/index.html @@ -17,7 +17,7 @@
0 Database query(s)
-

Sample app to demonstrate using an async cache in your API to save database SQL queries and speed up API calls

+

Sample app to demonstrate using an cache in your API to save database SQL queries and speed up API calls

Every 3 seconds we fetch the current time from the database, however because the sql query From 1df6a887521cf3faa20bd9ac5df9f3b6e7b5eb26 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 19:10:10 +0000 Subject: [PATCH 12/39] feat: updated artwork --- artwork/favicon.ico | Bin 0 -> 822 bytes artwork/logo-128.png | Bin 1139 -> 1708 bytes artwork/logo-32.png | Bin 551 -> 607 bytes artwork/logo-64.png | Bin 805 -> 977 bytes artwork/logo.pdn | Bin 7036 -> 11172 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 artwork/favicon.ico diff --git a/artwork/favicon.ico b/artwork/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b252115ceb6297b4da9cd0efc89c37590a4f9c4c GIT binary patch literal 822 zcmZ?rHDhJ~12Z700mK4O%*Y@C76%bW_#hZ2aD|_NRJ2W8xkW7jf+j0^1GP_4^vcw$ zn=9{#)uwE)$#czuJ5J6Wy`GoA|M z=IB;6swcw5;S4nOdUi*%bZgUkvD7X_;U#Ffb;d;MUB~-%o sUYAN3LG_pU7%Mb$+|8GlY2^di1l40IKoeOcYZYWN3r%p8OI`>706$dl%m4rY literal 0 HcmV?d00001 diff --git a/artwork/logo-128.png b/artwork/logo-128.png index e2ee16a4e789baf151255950c5a0f72f02748e71..6ea79161d4ed854329a074215335964113e0514a 100644 GIT binary patch literal 1708 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 D>5bHx diff --git a/artwork/logo-32.png b/artwork/logo-32.png index 052a7cbff73ebf31a11c67f7f4146a62ad4526b4..0b70988d4b736b5526081b0085bcca6ec6b55924 100644 GIT binary patch delta 532 zcmZ3^a-U^FSiNM4YeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu5ncao+r71_s9So-U3d z9^T0b5B57nx13vU<@7vQcH!rheHVU)xWA}brCefnRnThwD~`GTrO46`beLb7@_qW8 z`&8_p>x$+FYii4u)z?beRTe_H6BFwj*W8%L@U(NG)?O{4+ZUD;C|PjtXI;Z2o;I89 z@5fY@Rg6js$@6B~2MG8|im{|W+`izvhFH?ODDUS>pT*hN&S<<-G}p-_H9}xc>v5*U ziOTjh5e=Jc+dc|RMe{O=-7x|d70|0^FY*!Q}CgPjk01@!^HnaF}!|23pXyfufeAA_4f2mLFX=mrp+HB zE@X?aPxi|)&xM^1d}^PW@Xpl@5>=&w|3w+9&g>M?TsNI} QHUkiNy85}Sb4q9e09N$nb^rhX delta 475 zcmV<10VMw41g8X$R(~3FMObuGZ)S9NVRB^vP+@6qbS_RsR3LUUE;TMOF-P-4Hvj+u zmPtfGR7gwhl*=nbQ5eAeB`n0k#v~>Sl4Ou1&nb$;j)aAU6v^g1z9VC3l%>>UISVO` z4H1gfkhk)PB1&Ep<6GUnIkz#c+gRQDb?Wy0ZokEOq@}A^N`G+zNpS*6`QN~#oY@dv z26^h9PR%sH?z|X3;EgY=FveWCp7WX&-dhiQh7%W95(DTN6sj9Kr4*>UFz==^WkfC* z=3fFuaCyZmsi|Qrkg1~kyoqXNYz6RV>L4L8SPhX(^?`Wc5aevX3>m)Vz!9&b&Vm@z z1PyBeH{4Po%75}*>OpfftOZ(Og9;JetO;hU1-f9B3R}EG6UxXssB_IL%HfDQxb0|XG~AvG%2qa^dW z!LAJoTuxa*6Q-)RE}F*M{6eiskEwe?%b^A0E&bEoo4)Ov{iB}!Vfp9Z_n!aU#%_Nj z&8J6b5wDfyg_Dhbd?iH-CNs~KdGW+Sot-$LT|!#n`Ex#*WXqhfWoi7$x=Sc2|CfJk z&#~!Q!Yt3kUvM)&lls!iSj_wDP{U3(+ahM4XVdF@&l%7C$y@GSW4kofy!+L+-Tf)I zsub0@t5n!@%=g(%zy4!&nPB~V$-3*W^hB?0W(YarI9)Em@OyuF+K2KPXLp=WG~sm! z6e!urX!Z9-f~#KqfAQ^Ie=mr##O58a`yROOwA_r%+2x|kOLqsqlP{2;_d)&ha_LLE z7wR$XI?`bAw|@5e=r1+p?VEe~ucVcHT7FmXr}6@Co)twg54)A))LA6n*RB%#@y_4N zgvVJS=YlF*%!&L5-Rjr2I5ZS)OwgXM>9w1;af9P@i3{=5j*Hj!trkewvc_&1Yuy!{ zyVLG6f~5|~aqgRu$?eoIF}FTq<0kK|woz;9x!s!K_@So|MN4O^GaeUX6NI4Id9qKPP@OT!F*j+YY}?%HMo{2T#pi za3}cf4j!=E>I>E{nWVqzybgzf)iu+PGY?gM^}O@O>YVuRQ(TG%BtGrhdLTq$UTu{F z*z-%G=6`T)J73t#AF?9$Pv-k&y?<6Jyx7e4hyQi(`!gjEHGe(xzcsJeD$ZVNb&J;3 z*Yj7e{gYXHt@5qwQOD`h6H@s%u`~PKJU?rtgG`Lf*Jl~)HR@S+-C=N>yjj*S{czp) zHCmdKIwz)tCIG04q*VX_ delta 731 zcmV<10wn#>2c-s(R(~3FMObuGZ)S9NVRB^vP+@6qbS_RsR3LUUE;TMOF-P-4Hvj+v zlu1NERA@u(nZIikK@`XRODrsG1dXIHHU@;CDHIX42wo9?5fF)>@fV6{BRg+qLxR!3 zSy&}RB8riKibR595KRg}MJ%omV-bm9p^)NyFc6k+vb#5X4u5YW1D|4V?|t9xXSv;( z*Zz$cOHpe*YGHVTKhCPkBXzEyZg=bHP4O?tT2cji^<&#>RrZ@XsCVUndH0lhmyb9X z_JIx-0fBZ?dw;1;K*L;r zFJFsaf$R~0*A-A-07-#6L{MJpoSKt;6KrqSor~~Q?;8)>nO%*OZ+WS2=5SSthpX@Y<>K|q=yAWaaECJ0Cq1Z0Wpe*ptM-qiH8Nml>> N002ovPDHLkV1l|aLB{|9 diff --git a/artwork/logo.pdn b/artwork/logo.pdn index a347443af07f89e01dc2efff38923d24659e359f..42c361361e9be6f56a2d20ca4569a2cc38a605df 100644 GIT binary patch literal 11172 zcmd^lcUV-{x;I3nr~yqh*eGJbWC%0EFik0$!py+*1`HOQ-V4)b0LN(5SW%60;&95*~|*j8I6Ga)tSQMI&@bji!W1L}Y|XXq8IRm1ad6 z(4y4q5+dVb;OH1s92_14$HFj?iSc5K$*k8V#+wxutti5vlP5$f)9_4=4Mru)^*G>S zaQTX4z8rW<0jmVyPXIiLM3$C916yzz3@!(jf-{=p#3(k{z|pb!a6B;9uCiG%0yZCP zOcf_9GQ~Dvril(z(L|hBV$CLl6QmhjJ_nyhQAp7eI8sF7q><g4CAysXb zqdDwYA<1M&Q{efKQaHds6+SMLOtd)|6vex#QtjZt|F1G^nHZDJT&+g*e!l z!4tf1q@rkqR3aZ7$zuyL=>mKdp!VudaZFfJQO!jP+tY!(~A#8}J-8cf8Jr12eO3ll4|(lRVaEQdw0C`}Y6 zfYy}dnC0fvsRH%@d3~&{z zKyfrUx+qpAQJ9T*2t$aqWl~@cgM`TvNzGKaUe8p>i9(WroQ&gTTTn!^G?t3AbF6Ho zmCWJO@gjDnfkvmq(NqvKgn*J{rx{3Gt3B2X@`n?#c?KtrWJJ-#bT~RaTgG5z#-{RM zv2mPirJPE}G4!c0xEv>jvtVW{T%+Q_ojkOa$c`mviaB<+jl{rd)$}-_Jk>(h#i5Nt zF+@ZZu*7mDMVE=j$}mJIROjT$@j4k)OJ&)GSTipcD&b@#TeAgRrJTqmNa$=L(P2R2 z%$Y>1)`oS!99FAcg_g=wQ!)^AD_oDp#iC>+yGkNgVR?Wp;Dl_aLW#@7h-LD0b1FtJ zhMFukJ35tY6hfuuSPm4Ooh;!>q!4&4ft3d8*_p{=~HJfBfL8*=Da3~Vaz;c{%C=(D8kP2YwgC@s#J&820>BJoTUT5Mt2=xBU~T8DKir3M}WM#m#~@>C|9NV2DyZA2l* zYD7wH9GFB%M@W=L6h}ozOB6Byl}g2lQXR<@J&mNq3*!_h0n-2#;iYkAOsau`OjX9o zSaz9_Z`0s$3>cC?G!aRROqqggNKIFz>+ler9m5pD1X4&Q%Bqc1!m|bG3ON-e=A=4s zOq9_ng^;v1v(acGqGU8DzzrTPRY79WOh_yb6AP1Zuu2@1z*6IwHnT993`gQ%R5U#q z1!dYLOm?csq)kaiGl&ujPo#7&&DY=aUu*WTb)gjJFyxZZ4eHp zhT~EgG#!R!RNLrO1SVN3)F$ioDKL$mohfHq)!9}GnoB{!B_>iFoQO+@S-3ntj>t)) znPkRPsD_12qhf^s2|@}ZH5*43z@S(yI0p_BrwM3qK#>N$7^c4n z6UPR8LJx65XmE#sOcKS?S!U37=|V0a;5Nm9N!G|DsN@WbULgeh%m9$B$72a*G!3c{ zSdCn=0j+~zNKS%7k^(cL&}t48ZUHQb$F>_%**FLR%Mz)Da3oR9f-+cG89<(d%9cSh zP?Z|6b~rIVo6EYabOgt zjitcIcuE9CXH>`X@D8F`s3qwIc#AHDEyl61cplt9gz2r&xHuvPZBJ!8;Z6vIP2y|e z03j9?nJbp#g;XOQ?PTIv=nRtr%I89%Vknkxby$%kI`%Jk7z@IIMaq;awU&iK2;v}2 zC)JXvLdS9y1_5NyApmYf2EZY3c&k&SAZH}YxM~PfC!oM7W`)g8q*yf&9Y;uz2(_S2 zEIKt;D#tS^Ivm!hXV6(JHXfZ!)~1;qL{=s>UFFnEQEIHD27&ILeL_*Z2X&^Kq++>%kG9fI98YblPV$=A%Yz$hW z(L*xjB1uYyNrdJp!MLd;z_4Y*%|6G~3c)M|{` z8L>K~+JFbV#YxDt#36xQ0CAD6G+Wse8%YikDV+d^2o}VtvI~(G7MW#KG64~KeKIV? zD5T?2Bq59;v)WR#jUtK|Wl~z48U)-XAZX=0w#BT+(Htm@CLLsnr4fI*k}kIKP3a3N`t`Asb(xM*=}$m zrFID#@NT9(!$d{!U?QV~A>_eOQk%_6N`X?XR+|Bi6=9(|mOKNRA)yds1!+`-N~Iz> z#ZIwRD}$ullBEi{5iYV?co}JySfQbuPESQ>h?xiknSlZ` zNgO_v!$&3?)v0p1JRu=60-6{P{WFn;{+S^s`uGBWK0aRH0-xYD@*|*Nu!KsTnW#52 zq~;i+&@7CO_%lELYkC|VL9l4d7NaylC$*T3LQQl8%OcV!#Z;+-r&mjL2|9~LGYkYD z^A}9L*rJu{%zi+}aHWZ;G#T_Jsl@MHWpI;l}9 zjsX^(Ce&D@0Gx4t?Cja^hjPtErA{6lp*4y1K;#sCNCqqh`1|)Ph|p*uQjA2wv4}V+ z41=8;{r?ht?(DhX=6wQ(0L8-hcObx(Pv9`17zQ@~YtFzieg>ftP|YZu(I|9)L_)HZ zAlbnmG?aQqS*n0hWQFEd=@l1-J_EITRH74{91*M;2YE(i{D< z=t>>(9ys>V!V%!8Nl-gw>W6lV9$^z{}Bow`UY^} z|BS7IMl?h%bquqDuIlUS7YH;ApDML0Wie)<9N{+{s1xJCdW2E}eB{HBkr5go5=Fw% zkr8HxL7EU58yPV;J0Vg`CK*IJwnd~(gQXDJ7CIr$&amMIBc^W;Kq^SA-^jm+i1Ygx zs6+oQA{L8!F9HC5Cjt0r;k^W${5@LNpJ>5^;GSg`mED29&=0m{Fe4g|eX z`e8u)@W+#X9~TEt2T(@HhanuVG;4*1!9YHEKp2dAgVbnNN=^PIi%}*NOGgcMLjHbo zjCj{DcyNq-_x|qqct7&*5QE`n=$HS6Jje&KS$`7?+T5of&fvaV2FQkQK)~b=yWR;O zEdL5H82!cpGXjA~^+8;o0D^!N4{|*bcnO>Y6g~^z-+`gPi!Vt3;D9jTB`_Q){yrcA zs0;&<|986rBeA~-un7s%Fm(Edd*A~HE&g4H3Bu4DwL;DND!7TkjZ6k*f*bi)ynvDS zFNe6F2{=O~(qSgCPzM7#hXR57FDj=?wR)oiuhEOuAgSQJ@cj+S3ycIR!$79~J=dVg z4Alv>(tpku{K2CG*zriE2`?0@ zT%bZ|VhyHR0TQW9YBWkEKzE?8FKGP0ItNQ1Uw@G3KWqOf`HU}U79jh+pcR0W{*3}( zUr@h;eV{u3+BX95TX5{)B@bU2F!(am=jC}{;LHvf5;5^^MQ#6sh;txH|F5C z1)@ER_Wct1>PFbm(PKvZurF%Z=$nfbN4|Y@c15z=2j~83^oyO}f3vC3mYa*7P1VwR zH!rV3lyxV$GbWrXtG?>8TCD^!aohV%|C`5WOQ<8*ofj6g3UIC);jjD${`qn3S^KU1 zcVafSZYy9npC5Abvx22%bGMYviB#90h(5CYi0YwczC$! z{Lu%_*E65hdSU{%iYphl^vMIi>&(uHPv53G^W`fjyjWkn{AJ>yhP)c=!#exOLq{57 z-m32}%kh-99jo^5&D4hvS}I-Xo1a-iGsfH^IM?oUOApj##jwDE#^n9p1aE zX<`9aRFL)A9#^fphW^Z3+-KeRq7)hitvgjaywg+>Jn?b$yqFSFO?l0!5d*WD2cBQ) zsXs@|^_FX|mwa*(7B(VhW9u1R{Sr@M>%b$erTe)7qd$NR8+2Df+Jdy%%PkI(e>UxVrtew)eKG zHIe?Jo%A%iXO7r7?|IL$c<$lCC$@}UrNle6f{(35Z>$Ob{^^09W1?l2BGPN-`SX`g z&O7X$l<593!#lS%arKM#l?5Bx2ZFCX#wHf!yo8MnuUM1+3hfvODSF;?SzlCXif_&J z(3;1s$ln#S?#hF2x9RE^dl1b{%KDzohe{e;13&2o#J?kY5LFjy`imRy{N!~C_7pw8 zF}!u4wp~@5R2asoj*Fxo?#-$=IM>x!jze?bTyZvjyy$)pv#aN_=ewGI-Kg8et`l4| z6EpuzyC3f5+r5uA4!BAhY>RhT2M{L(4v+Wz`x*CZXJmQLzN=I}`J{)2uBv(`*0s6a zm1!*R@wD1XT9aa;tlpxZL_hAH+t=@EemcD1(#e|s#?1P+Hu>{*Q~y$j&ZThfP#gOqfqZ!jew^F589uPF{b)E{@ub@xE-K>PmkdLh1ZQr+SH zxTCHwrT}^@Si-}LPhLz>|JIFn z3n1GoUT?^due5}`4c}fJGV6<rE=EATqEMGSEpA0Y2edWsYPBHa& z*ng>b)NSz`|M_?6(OXXMY*YH>l$s`_iyHGXY{Z3zw!A`o;=$73Wixx1uc+w$h1vyg z8%Vl42X*q6BY=->A`WNNT8MEuVFSTv`spa|R(qrA${YeWNr*}IK9DG|lH;+-k`GPglZ}Hjr zuyOy*gEVpxYkSZpGjv=7iq9aAsk_Wbd*++UXrH@QuhQyIhr{SQUszj_s<~M#Oh-|ERrf z*z?{zf4=GNb2r-Iowy}`WATZoeHY43J#}2l-Z}j>sfhV(F1qfs{xb)#FC-pZP2UpC z@C#@6bS9C$T;`p=#$3QZeeCewZCvEC!pljQu%%yak1Rh=P*v<& z+Jo62?V%^FENyM3hfmml6F2v8;j6t}3l<$z8^W%hcsTag){gd4U#icMqO5hb)81Bb zyWbiwJzl=zMNIow?$#f6s5@ssUP?;h-B$F0${$+>F1%PDysPWf?Tc0Is)W;P z;*MW@x_|82S2o_Ytf;PFY5eewf=|gedXgutZoOHW(&nC0;(ycQ`u=Dhe0|f%ITiNx zg=wJ~NLOf;>+q4!I~*G~*L@rwLXWy^><;RusDbK^sNzb|C z3a#=I&Krjjo5=5^hhQXTnZl6LRv)^f4W)cQ~PCiJ#9)widx+9%t) zUmrP3EE%p_Y2TFVPc6z^cJAqlPY*QdFx}y_T=m8snNG$F)nVux!}#g|>x zZ%!a@Rw=zHdwE z!(WrGZoUq=niJ%id@Z=c_7oa_<&Ui6uKX$8v*~$fzpuOdqpD$Mp=<4|)lK|(Kg{%R ze`~wp)sHnp1w-yXBqhJ8Z6y%H&Uj8;KDO=@B5MEnN5lgCxV9NmPx$^XpS`9P7Z3!{ zYu8-kvTiXd)?HF)2tB^e=(Ptz3V7E9mEBn(wV!Oek>kAi1HZc~;B?+bMgPg>CRtv< z0)RpRrU#rKM4{Ap|%N@49H>KuSRm%`yOqF|adsflf zUTSa7Z|uEqpDTzSzCZu7 zTNCdiv9jj{d0*YhwZwa?W(L>9RQ252LCsqlZTiNgY7g7)-FNutzAGmg9(9X}-+iak zZ96zNP`BXejjt0YZh916m>>20NyLT=y_4L*{bxLL+#$dEY(AkU)pR|)SNhhKwW8C> zJ6LXbm|c@R;+%v%jlr~9v2WgILp?!vi$|}#iZvXp9{YP{ z>q@u2?$pxN*PeH+&EM5-j%r>?nvnX?b^2|D+q*fpy;A%TdOWE2yA9>~?=QRNUjM3L z{OFtk!Ub1kI=$0{#y%c$YSIe#g5=aJL5x>L-W{}^k7S--?cE*Sk20=#d3F5F6J<$f zI(?s?>Qo)Pio0>=f@^oVrefE{v};_YyJ&aAtPI13=RvICpj+kfu*RX^iuWVN3yfE8 zAG;UE$yxrWzdKjx#p(QMmVY6q3nm8u^G%am~5oESNoA6n^`4X8TNjE9!*xGUEQ5<1LF0^n@EdyBc~9)@Vm*#}t#y0o0ZSTwzu{b?t2ppd=;^*66>fUf9`#KBcJ?hY^%c6% zzG@?@fm@Pr)fHBE8$Pvn-TlS6#m%dk_${>^Gk3J`>n^x+?vv{GkG_0WAMq@w|LIe( zsozlS3@EA?lk2H;UnX5AH{|zhVc&TT_ilbLH}#i+d)UiOU;Pl&cj0#7j+!N}_MUwp zlzkGF7XRIE@-e1|_o8P!Uc9)fC2xs#QgPy))AAS0Ynyl@e3<7Ep}TZ`uUrr3cLew3 zeH7mxda1ne!NkzZWs97-pO3A)MYZKc35?MCk2`vCrYPl%rimwazFAP#7=5Gn^W_O= zq_6hTmnZ%)2i<~_o};R_M~>3eC?W~eaEBECEdQ3|7(Tb+*;IL z_Cr&`iV(+!m237oSd-f9)}_>cdK@5))>zqex3f}`jn63{H&k%oSm{Xc4yRm@=h&w zOljHEgLy}c9giB>HNM9S7!SC?T@{6UhIBR@v4$Cc%$mKsO*XbNc&vAU^I}D^d7F9< zwdr91xf9FiL-uSOL#`ele&=vvQI&ULzk0N8O>c9SXF^KH7NC3G@+RGL+pe`lF}$e%96uA7|*#2YXj`T-_cbc9S1X-1lJ1g!C})FGW2~ zF1aWyE2Mf+VQDG8y3jx3UQSWb3+eGcetlu3yr^69=)!}i;@6`uba!~NuAUOjDF2k0 z{qg6O4&wGwzxomBze%USi*CFQZ(md(e(*Z9VNVy3u#q>U?3cG}&2}2RZ0MLQqmQpuUnu<5#Vrp-pgF(azccIipGMt>%!`bTk z!g_GamdjVRKc29!q<-e`S<^$Vd!I$Vy!2u}vCC}oB#hziDA$b0tIi}`uE6Gt?q$3c zI3MrrEN?FMXbIbwUZ&(t^mQaGG<5$+4lSwP7vV;CKO>lryzb6yUA>8RyK(GWZS+=` zB(yX}-|>Z)@=|w^KVCiVSsBbVt$!g!`+Z;e__E0I5%-#XCWb`xJ!qV0nQMc2N$Wyux7((*RlSo!U=mw$xYFaExAc1OG@w*phrJn!d$U3mw0 z#kZ4AOdXi=rXl})JM^w4H0gZPYu_X4FWZt9BCfsN5|cHKiR&wS2jCfs@5IbtdZqfSJ*niH?Fwtzf3uErr^8VUvIsYSGL(CZ0+5d z8w2enh5drN)Z8A#7#Ou8XCI>K$-Py-T|Bt{7%sJFT17<9#3p&zp1!@;kul%(M$i0l z(%Qxy9r-V>&3vP;@r?{CyU{%DC!}TahJv;5;{^DfX^oppmoIs;YUMiLP9tn5@ZXkU z(>>ztZ>LrVohzr0niV2>LO7(AFPfRo`o{Gtch78n-t^h-V>{YTR#(pM%g;Lc*`+>l z+i6Q#zLT8OH{RQQs~OsDx!W$jZEQO})RUMsw`|(Dy@|8Fb=QgCj`Vz*lT(YUjeqde zMK61>*L@@V*-1#*)v#sdZ`VHP>crHw-#t2NdB1LN_qSC+M_;FwPcz=%Q88?0&0+mc zk9T*ZJNV~5Nxr?g^xTOS_2R0$l|l6?!XGx*j9uLC7F2Ez` f$A#BJ4md}>Hs;R!r!O{p$YbxW!re{aH|75U%Jj)> literal 7036 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 Date: Sat, 3 Mar 2018 20:14:36 +0000 Subject: [PATCH 13/39] feat: basic service registration ind --- .../Controllers/DbTimeController.cs | 5 ++--- CacheDatabaseQueriesApiSample/Startup.cs | 10 +++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs b/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs index 572e90f..8479d15 100644 --- a/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs +++ b/CacheDatabaseQueriesApiSample/Controllers/DbTimeController.cs @@ -11,11 +11,10 @@ public class DbTimeController : Controller private readonly DbTimeContext dbContext; - public DbTimeController(DbTimeContext context) + public DbTimeController(DbTimeContext context, IAppCache cache) { - // this could (and should) be passed in using dependency injection - cache = new CachingService(); dbContext = context; + this.cache = cache; } [HttpGet] diff --git a/CacheDatabaseQueriesApiSample/Startup.cs b/CacheDatabaseQueriesApiSample/Startup.cs index 8af2581..4fd9f8f 100644 --- a/CacheDatabaseQueriesApiSample/Startup.cs +++ b/CacheDatabaseQueriesApiSample/Startup.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Builder; +using LazyCache; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -19,9 +20,16 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddMvc(); + + // just for demo - use app settings for db config var connection = @"Server=(localdb)\projectsv13;Database=Master;Trusted_Connection=True;ConnectRetryCount=0"; + + // register the database services.AddDbContext(options => options.UseSqlServer(connection)); + + // add a single instance of the cache (transiant would also work as default cache is shared) + services.AddSingleton(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. From 45648a66645532dc70b8745255ee5634e5e22f49 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 21:32:30 +0000 Subject: [PATCH 14/39] feat: add LazyCache.AspNetCore DI pkg --- .../CacheDatabaseQueriesApiSample.csproj | 1 + CacheDatabaseQueriesApiSample/Startup.cs | 5 ++-- .../LazyCache.AspNetCore.csproj | 24 ++++++++++++++++ .../ServiceCollectionExtensions.cs | 28 +++++++++++++++++++ LazyCache.sln | 10 +++++-- 5 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 LazyCache.AspNetCore/LazyCache.AspNetCore.csproj create mode 100644 LazyCache.AspNetCore/ServiceCollectionExtensions.cs diff --git a/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj b/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj index 8174edb..8e82000 100644 --- a/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj +++ b/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj @@ -14,6 +14,7 @@ + diff --git a/CacheDatabaseQueriesApiSample/Startup.cs b/CacheDatabaseQueriesApiSample/Startup.cs index 4fd9f8f..7d891be 100644 --- a/CacheDatabaseQueriesApiSample/Startup.cs +++ b/CacheDatabaseQueriesApiSample/Startup.cs @@ -28,8 +28,9 @@ public void ConfigureServices(IServiceCollection services) // register the database services.AddDbContext(options => options.UseSqlServer(connection)); - // add a single instance of the cache (transiant would also work as default cache is shared) - services.AddSingleton(); + // Register IAppCache as a singleton CachingService + services.AddLazyCache(); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj new file mode 100644 index 0000000..28b24c5 --- /dev/null +++ b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + LazyCache + true + 2.0.0 + https://github.com/alastairtree + https://github.com/alastairtree + ServiceCollection regististrations fopr LazyCache to initialise the depndency injection + Copyright 2014 - 2018 Alastair Crabtree + https://github.com/alastairtree/LazyCache/blob/master/LICENSE + https://github.com/alastairtree/LazyCache + https://raw.githubusercontent.com/alastairtree/LazyCache/master/artwork/logo-128.png + https://github.com/alastairtree/LazyCache + LazyCache DependecyInjection ServiceCollection SingleTon Transient + + + + + + + + diff --git a/LazyCache.AspNetCore/ServiceCollectionExtensions.cs b/LazyCache.AspNetCore/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..86bd703 --- /dev/null +++ b/LazyCache.AspNetCore/ServiceCollectionExtensions.cs @@ -0,0 +1,28 @@ +using System; +using LazyCache; + +// ReSharper disable once CheckNamespace - MS guidelines say put DI registration in this NS +namespace Microsoft.Extensions.DependencyInjection +{ + public static class LazyCacheServiceRegistration + { + public static IServiceCollection AddLazyCache(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + services.AddSingleton(); + + return services; + } + + public static IServiceCollection AddLazyCache(this IServiceCollection services, Func implmentationFactory) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + if (implmentationFactory == null) throw new ArgumentNullException(nameof(implmentationFactory)); + + services.AddSingleton(implmentationFactory); + + return services; + } + } +} diff --git a/LazyCache.sln b/LazyCache.sln index 1c33f81..086b371 100644 --- a/LazyCache.sln +++ b/LazyCache.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27130.2024 +VisualStudioVersion = 15.0.27130.2036 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LazyCache", "LazyCache\LazyCache.csproj", "{E6A1EF20-94AD-4A1C-9A89-3B2FA8AD8EC7}" EndProject @@ -17,7 +17,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "info", "info", "{81C0E096-5 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{335BA426-C839-4996-8476-F3EE4056C40E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheDatabaseQueriesApiSample", "CacheDatabaseQueriesApiSample\CacheDatabaseQueriesApiSample.csproj", "{5D6A88DD-230C-4057-B8EB-A987FF4F29DB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheDatabaseQueriesApiSample", "CacheDatabaseQueriesApiSample\CacheDatabaseQueriesApiSample.csproj", "{5D6A88DD-230C-4057-B8EB-A987FF4F29DB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyCache.AspNetCore", "LazyCache.AspNetCore\LazyCache.AspNetCore.csproj", "{A7B07002-29F5-4463-8CA7-097C337337A1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -37,6 +39,10 @@ Global {5D6A88DD-230C-4057-B8EB-A987FF4F29DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {5D6A88DD-230C-4057-B8EB-A987FF4F29DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D6A88DD-230C-4057-B8EB-A987FF4F29DB}.Release|Any CPU.Build.0 = Release|Any CPU + {A7B07002-29F5-4463-8CA7-097C337337A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7B07002-29F5-4463-8CA7-097C337337A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7B07002-29F5-4463-8CA7-097C337337A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7B07002-29F5-4463-8CA7-097C337337A1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 41b0e9b8501d86db8e6e9ab0b301f90308f21c13 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 21:32:58 +0000 Subject: [PATCH 15/39] bug: fix intermittent test caused by race condition --- .../CachingService_MemoryCacheProvider_Tests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs b/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs index 3cffdfb..e8b0afc 100644 --- a/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs +++ b/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs @@ -202,7 +202,11 @@ public void GetFromCacheTwiceAtSameTimeOnlyAddsOnce() var t2 = Task.Factory.StartNew(() => { - sut.GetOrAdd(TestKey, () => throw new Exception("Should not be called!")); + sut.GetOrAdd(TestKey, () => + { + Interlocked.Increment(ref times); + return new DateTime(2001, 01, 01); + }); }); Task.WaitAll(t1, t2); From 5d4efda1a08d091de76ff95fa1ec35088b57e638 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 21:50:08 +0000 Subject: [PATCH 16/39] tests: migrate commented callback tests --- ...achingService_MemoryCacheProvider_Tests.cs | 97 +++++++++++-------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs b/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs index e8b0afc..ea35dfe 100644 --- a/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs +++ b/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs @@ -425,30 +425,53 @@ public void GetOrAddAsyncWithALongTaskReturnsBeforeTaskCompletes() Assert.That(actualResult.IsCompleted, Is.Not.True); } - // TODO: restore this test - //[Test, MaxTime(20000)] - //public async Task GetOrAddAsyncWithCallbackOnRemovedReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() - //{ - // Func> fetch = () => Task.FromResult(123); - // CacheEntryRemovedArguments removedCallbackArgs = null; - // CacheEntryRemovedCallback callback = args => removedCallbackArgs = args; - // await sut.GetOrAddAsync(TestKey, fetch, - // new MemoryCacheEntryOptions - // { - // AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), - // PostEvictionCallbacks = { callback } - // }); + [Test, MaxTime(20000)] + public async Task GetOrAddAsyncWithPostEvictionCallbacksReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() + { + object callbackValue = null; + var memoryCacheEntryOptions = new MemoryCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100) + }; + memoryCacheEntryOptions.RegisterPostEvictionCallback((key, value, reason, state) => + { + callbackValue = value; + }); + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(123), memoryCacheEntryOptions); - // sut.Remove(TestKey); //force removed callback to fire - // while (removedCallbackArgs == null) - // Thread.Sleep(500); + sut.Remove(TestKey); //force removed callback to fire + while (callbackValue == null) + { + Thread.Sleep(500); + } - // var callbackResult = removedCallbackArgs.CacheItem.Value; - // Assert.That(callbackResult, Is.AssignableTo>()); - // var callbackResultValue = await (Task) removedCallbackArgs.CacheItem.Value; + Assert.That(callbackValue, Is.AssignableTo>()); + var callbackResultValue = await (Task) callbackValue; + Assert.AreEqual(123, callbackResultValue); + } - // Assert.AreEqual(123, callbackResultValue); - //} + [Test, MaxTime(20000)] + public async Task GetOrAddAsyncWithPostEvictionCallbacksReturnsTheOriginalCachedKeyEvenIfNotGettedBeforehand() + { + string callbackKey = null; + var memoryCacheEntryOptions = new MemoryCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100) + }; + memoryCacheEntryOptions.RegisterPostEvictionCallback((key, value, reason, state) => + { + callbackKey = key.ToString(); + }); + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(123), memoryCacheEntryOptions); + + sut.Remove(TestKey); //force removed callback to fire + while (callbackKey == null) + { + Thread.Sleep(500); + } + + callbackKey.Should().Be(TestKey); + } [Test] public async Task GetOrAddAsyncWithOffsetWillAddAndReturnTaskOfCached() @@ -560,25 +583,23 @@ public void GetOrAddWillNotAddIfExistingData() Assert.AreEqual(0, times); } - //[Test, Timeout(20000)] - //public void GetOrAddWithCallbackOnRemovedReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() - //{ - // Func fetch = () => 123; - // CacheEntryRemovedArguments removedCallbackArgs = null; - // CacheEntryRemovedCallback callback = args => removedCallbackArgs = args; - // sut.GetOrAdd(TestKey, fetch, - // new MemoryCacheEntryOptions - // { - // AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), - // RemovedCallback = callback - // }); + [Test, MaxTime(20000)] + public void GetOrAddWithPostEvictionCallbackdReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() + { + object cacheValue = null; + var cacheEntryOptions = new MemoryCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), + }.RegisterPostEvictionCallback((key, value, reason, state) => cacheValue = value); + sut.GetOrAdd(TestKey, () => 123, + cacheEntryOptions); - // sut.Remove(TestKey); //force removed callback to fire - // while (removedCallbackArgs == null) - // Thread.Sleep(500); + sut.Remove(TestKey); //force removed callback to fire + while (cacheValue == null) + Thread.Sleep(500); - // Assert.AreEqual(123, removedCallbackArgs.CacheItem.Value); - //} + cacheValue.Should().Be(123); + } [Test] public void GetOrAddWithOffsetWillAddAndReturnCached() From 1ba247ff9f21dc46498b9cd0d169a1be42e9c013 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 22:04:22 +0000 Subject: [PATCH 17/39] tests: tidy up and bring into latests c# style --- ...CachingServiceMemoryCacheProviderTests.cs} | 256 +++++++++--------- 1 file changed, 125 insertions(+), 131 deletions(-) rename LazyCache.UnitTests/{CachingService_MemoryCacheProvider_Tests.cs => CachingServiceMemoryCacheProviderTests.cs} (82%) diff --git a/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs similarity index 82% rename from LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs rename to LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs index ea35dfe..028ea53 100644 --- a/LazyCache.UnitTests/CachingService_MemoryCacheProvider_Tests.cs +++ b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs @@ -10,7 +10,7 @@ namespace LazyCache.UnitTests { [TestFixture] - public class CachingService_MemoryCacheProvider_Tests + public class CachingServiceMemoryCacheProviderTests { [SetUp] public void BeforeEachTest() @@ -37,8 +37,8 @@ private static CachingService BuildCache() private class ComplexTestObject { - public const string SomeMessage = "testing123"; - public readonly IList SomeItems = new object[] {1, 2, 3, "testing123"}; + public readonly IList SomeItems = new List {1, 2, 3, "testing123"}; + public string SomeMessage = "testing123"; } private const string TestKey = "testKey"; @@ -47,11 +47,15 @@ private class ComplexTestObject [Test] public void AddComplexObjectThenGetGenericReturnsCachedObject() { + testObject.SomeItems.Add("Another"); + testObject.SomeMessage = "changed-it-up"; sut.Add(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] @@ -171,6 +175,19 @@ public void AddWithSlidingThatExpiresReturnsNull() Assert.IsNull(sut.Get(TestKey)); } + [Test] + public void DefaultContructorThenGetOrAddFromSecondCachingServiceHasSharedUnderlyingCache() + { + var cacheOne = new CachingService(); + var cacheTwo = new CachingService(); + + var resultOne = cacheOne.GetOrAdd(TestKey, () => "resultOne"); + var resultTwo = cacheTwo.GetOrAdd(TestKey, () => "resultTwo"); // should not get executed + + resultOne.Should().Be("resultOne", "GetOrAdd should execute the delegate"); + resultTwo.Should().Be("resultOne", "CachingService should use a shared cache by default"); + } + [Test] public void GetCachedNullableStructTypeParamReturnsType() { @@ -221,24 +238,10 @@ public void GetNullKeyThrowsException() act.Should().Throw(); } - [Test] - public void DefaultContructorThenGetOrAddFromSecondCachingServiceHasSharedUnderlyingCache() - { - var cacheOne = new CachingService(); - var cacheTwo = new CachingService(); ; // second instance - - var resultOne = cacheOne.GetOrAdd(TestKey, () => "resultOne"); - var resultTwo = cacheTwo.GetOrAdd(TestKey, () => "resultTwo"); // should not get executed - - resultOne.Should().Be("resultOne", "GetOrAdd should execute the delegate"); - resultTwo.Should().Be("resultOne", "CachingService should use a shared cache by default"); - } - [Test] public void GetOrAddAndThenGetObjectReturnsCorrectType() { - Func fetch = () => testObject; - sut.GetOrAdd(TestKey, fetch); + sut.GetOrAdd(TestKey, () => testObject); var actual = sut.Get(TestKey); Assert.IsNotNull(actual); } @@ -246,8 +249,7 @@ public void GetOrAddAndThenGetObjectReturnsCorrectType() [Test] public void GetOrAddAndThenGetValueObjectReturnsCorrectType() { - Func fetch = () => 123; - sut.GetOrAdd(TestKey, fetch); + sut.GetOrAdd(TestKey, () => 123); var actual = sut.Get(TestKey); Assert.AreEqual(123, actual); } @@ -255,8 +257,7 @@ public void GetOrAddAndThenGetValueObjectReturnsCorrectType() [Test] public void GetOrAddAndThenGetWrongtypeObjectReturnsNull() { - Func fetch = () => testObject; - sut.GetOrAdd(TestKey, fetch); + sut.GetOrAdd(TestKey, () => testObject); var actual = sut.Get(TestKey); Assert.IsNull(actual); } @@ -287,11 +288,12 @@ public void GetOrAddAsyncACancelledTaskReturnsTheCacelledTaskToConsumer() [Test] public void GetOrAddAsyncAFailingTaskDoesNotCacheIt() { - Func> fetchAsync = () => - Task.Factory.StartNew( - () => { throw new ApplicationException(); }); + Task FetchAsync() + { + return Task.Factory.StartNew(() => throw new ApplicationException()); + } - Assert.ThrowsAsync(async () => await sut.GetOrAddAsync(TestKey, fetchAsync)); + Assert.ThrowsAsync(async () => await sut.GetOrAddAsync(TestKey, FetchAsync)); var stillCached = sut.Get>(TestKey); @@ -301,8 +303,7 @@ public void GetOrAddAsyncAFailingTaskDoesNotCacheIt() [Test] public async Task GetOrAddAsyncAndThenGetAsyncObjectReturnsCorrectType() { - Func> fetch = () => Task.FromResult(testObject); - await sut.GetOrAddAsync(TestKey, fetch); + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(testObject)); var actual = await sut.GetAsync(TestKey); Assert.IsNotNull(actual); Assert.That(actual, Is.EqualTo(testObject)); @@ -311,8 +312,7 @@ public async Task GetOrAddAsyncAndThenGetAsyncObjectReturnsCorrectType() [Test] public async Task GetOrAddAsyncAndThenGetAsyncWrongObjectReturnsNull() { - Func> fetch = () => Task.FromResult(testObject); - await sut.GetOrAddAsync(TestKey, fetch); + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(testObject)); var actual = await sut.GetAsync(TestKey); Assert.IsNull(actual); } @@ -320,12 +320,18 @@ public async Task GetOrAddAsyncAndThenGetAsyncWrongObjectReturnsNull() [Test] public async Task GetOrAddAsyncFollowinGetOrAddReturnsTheFirstObjectAndIgnoresTheSecondTask() { - Func> fetchAsync = () => Task.FromResult(new ComplexTestObject()); - Func fetchSync = () => testObject; + ComplexTestObject FetchSync() + { + return testObject; + } - var actualSync = sut.GetOrAdd(TestKey, fetchSync); - var actualAsync = await sut.GetOrAddAsync(TestKey, fetchAsync); + Task FetchAsync() + { + return Task.FromResult(new ComplexTestObject()); + } + var actualSync = sut.GetOrAdd(TestKey, FetchSync); + var actualAsync = await sut.GetOrAddAsync(TestKey, FetchAsync); Assert.IsNotNull(actualSync); Assert.That(actualSync, Is.EqualTo(testObject)); @@ -340,8 +346,7 @@ public async Task GetOrAddAsyncFollowinGetOrAddReturnsTheFirstObjectAndIgnoresTh public async Task GetOrAddAsyncTaskAndThenGetTaskOfAnotherTypeReturnsNull() { var cachedAsyncResult = testObject; - Func> fetch = () => Task.FromResult(cachedAsyncResult); - await sut.GetOrAddAsync(TestKey, fetch); + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(cachedAsyncResult)); var actual = sut.Get>(TestKey); Assert.Null(actual); } @@ -350,8 +355,7 @@ public async Task GetOrAddAsyncTaskAndThenGetTaskOfAnotherTypeReturnsNull() public async Task GetOrAddAsyncTaskAndThenGetTaskOfObjectReturnsCorrectType() { var cachedAsyncResult = testObject; - Func> fetch = () => Task.FromResult(cachedAsyncResult); - await sut.GetOrAddAsync(TestKey, fetch); + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(cachedAsyncResult)); var actual = sut.Get>(TestKey); Assert.IsNotNull(actual); Assert.That(actual.Result, Is.EqualTo(cachedAsyncResult)); @@ -416,41 +420,45 @@ public async Task GetOrAddAsyncWillNotAddIfExistingData() public void GetOrAddAsyncWithALongTaskReturnsBeforeTaskCompletes() { var cachedResult = testObject; - Func> fetchAsync = () => - Task.Delay(TimeSpan.FromMinutes(1)).ContinueWith(x => cachedResult); - var actualResult = sut.GetOrAddAsync(TestKey, fetchAsync); + Task FetchAsync() + { + return Task.Delay(TimeSpan.FromMinutes(1)) + .ContinueWith(x => cachedResult); + } + + var actualResult = sut.GetOrAddAsync(TestKey, FetchAsync); Assert.That(actualResult, Is.Not.Null); Assert.That(actualResult.IsCompleted, Is.Not.True); } - [Test, MaxTime(20000)] - public async Task GetOrAddAsyncWithPostEvictionCallbacksReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() + [Test] + public async Task GetOrAddAsyncWithOffsetWillAddAndReturnTaskOfCached() { - object callbackValue = null; - var memoryCacheEntryOptions = new MemoryCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100) - }; - memoryCacheEntryOptions.RegisterPostEvictionCallback((key, value, reason, state) => - { - callbackValue = value; - }); - await sut.GetOrAddAsync(TestKey, () => Task.FromResult(123), memoryCacheEntryOptions); + var expectedFirst = await sut.GetOrAddAsync( + TestKey, + () => Task.FromResult(new DateTime(2001, 01, 01)), + DateTimeOffset.Now.AddSeconds(5) + ); + var expectedSecond = await sut.Get>(TestKey); - sut.Remove(TestKey); //force removed callback to fire - while (callbackValue == null) - { - Thread.Sleep(500); - } + Assert.AreEqual(2001, expectedFirst.Year); + Assert.AreEqual(2001, expectedSecond.Year); + } - Assert.That(callbackValue, Is.AssignableTo>()); - var callbackResultValue = await (Task) callbackValue; - Assert.AreEqual(123, callbackResultValue); + [Test] + public async Task GetOrAddAsyncWithPolicyAndThenGetTaskObjectReturnsCorrectType() + { + var item = testObject; + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(item), + oneHourNonRemoveableMemoryCacheEntryOptions); + var actual = await sut.Get>(TestKey); + Assert.That(actual, Is.EqualTo(item)); } - [Test, MaxTime(20000)] + [Test] + [MaxTime(20000)] public async Task GetOrAddAsyncWithPostEvictionCallbacksReturnsTheOriginalCachedKeyEvenIfNotGettedBeforehand() { string callbackKey = null; @@ -466,46 +474,48 @@ public async Task GetOrAddAsyncWithPostEvictionCallbacksReturnsTheOriginalCached sut.Remove(TestKey); //force removed callback to fire while (callbackKey == null) - { Thread.Sleep(500); - } callbackKey.Should().Be(TestKey); } [Test] - public async Task GetOrAddAsyncWithOffsetWillAddAndReturnTaskOfCached() + [MaxTime(20000)] + public async Task + GetOrAddAsyncWithPostEvictionCallbacksReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() { - var expectedFirst = await sut.GetOrAddAsync( - TestKey, - () => Task.FromResult(new DateTime(2001, 01, 01)), - DateTimeOffset.Now.AddSeconds(5) - ); - var expectedSecond = await sut.Get>(TestKey); + object callbackValue = null; + var memoryCacheEntryOptions = new MemoryCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100) + }; + memoryCacheEntryOptions.RegisterPostEvictionCallback((key, value, reason, state) => + { + callbackValue = value; + }); + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(123), memoryCacheEntryOptions); - Assert.AreEqual(2001, expectedFirst.Year); - Assert.AreEqual(2001, expectedSecond.Year); - } + sut.Remove(TestKey); //force removed callback to fire + while (callbackValue == null) + Thread.Sleep(500); - [Test] - public async Task GetOrAddAsyncWithPolicyAndThenGetTaskObjectReturnsCorrectType() - { - var item = testObject; - Func> fetch = () => Task.FromResult(item); - await sut.GetOrAddAsync(TestKey, fetch, - oneHourNonRemoveableMemoryCacheEntryOptions); - var actual = await sut.Get>(TestKey); - Assert.That(actual, Is.EqualTo(item)); + Assert.That(callbackValue, Is.AssignableTo>()); + var callbackResultValue = await (Task) callbackValue; + Assert.AreEqual(123, callbackResultValue); } [Test] public async Task GetOrAddAyncAllowsCachingATask() { var cachedResult = testObject; - Func> fetchAsync = () => Task.FromResult(cachedResult); + + Task FetchAsync() + { + return Task.FromResult(cachedResult); + } var actualResult = - await sut.GetOrAddAsync(TestKey, fetchAsync, oneHourNonRemoveableMemoryCacheEntryOptions); + await sut.GetOrAddAsync(TestKey, FetchAsync, oneHourNonRemoveableMemoryCacheEntryOptions); Assert.That(actualResult, Is.EqualTo(cachedResult)); } @@ -513,11 +523,18 @@ public async Task GetOrAddAyncAllowsCachingATask() [Test] public async Task GetOrAddFollowinGetOrAddAsyncReturnsTheFirstObjectAndUnwrapsTheFirstTask() { - Func> fetchAsync = () => Task.FromResult(testObject); - Func fetchSync = () => new ComplexTestObject(); + Task FetchAsync() + { + return Task.FromResult(testObject); + } - var actualAsync = await sut.GetOrAddAsync(TestKey, fetchAsync); - var actualSync = sut.GetOrAdd(TestKey, fetchSync); + ComplexTestObject FetchSync() + { + return new ComplexTestObject(); + } + + var actualAsync = await sut.GetOrAddAsync(TestKey, FetchAsync); + var actualSync = sut.GetOrAdd(TestKey, FetchSync); Assert.IsNotNull(actualAsync); Assert.That(actualAsync, Is.EqualTo(testObject)); @@ -583,24 +600,6 @@ public void GetOrAddWillNotAddIfExistingData() Assert.AreEqual(0, times); } - [Test, MaxTime(20000)] - public void GetOrAddWithPostEvictionCallbackdReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() - { - object cacheValue = null; - var cacheEntryOptions = new MemoryCacheEntryOptions - { - AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), - }.RegisterPostEvictionCallback((key, value, reason, state) => cacheValue = value); - sut.GetOrAdd(TestKey, () => 123, - cacheEntryOptions); - - sut.Remove(TestKey); //force removed callback to fire - while (cacheValue == null) - Thread.Sleep(500); - - cacheValue.Should().Be(123); - } - [Test] public void GetOrAddWithOffsetWillAddAndReturnCached() { @@ -618,8 +617,7 @@ public void GetOrAddWithOffsetWillAddAndReturnCached() [Test] public void GetOrAddWithPolicyAndThenGetObjectReturnsCorrectType() { - Func fetch = () => testObject; - sut.GetOrAdd(TestKey, fetch, + sut.GetOrAdd(TestKey, () => testObject, oneHourNonRemoveableMemoryCacheEntryOptions); var actual = sut.Get(TestKey); Assert.IsNotNull(actual); @@ -628,8 +626,8 @@ public void GetOrAddWithPolicyAndThenGetObjectReturnsCorrectType() [Test] public void GetOrAddWithPolicyAndThenGetValueObjectReturnsCorrectType() { - Func fetch = () => 123; - sut.GetOrAdd(TestKey, fetch, oneHourNonRemoveableMemoryCacheEntryOptions); + int Fetch() => 123; + sut.GetOrAdd(TestKey, Fetch, oneHourNonRemoveableMemoryCacheEntryOptions); var actual = sut.Get(TestKey); Assert.AreEqual(123, actual); } @@ -657,28 +655,24 @@ public void GetOrAddWithPolicyWillAddOnFirstCallButReturnCachedOnSecond() Assert.AreEqual(1, times); } - //[Test, MaxTime(20000)] - //public void GetOrAddWithPolicyWithCallbackOnRemovedReturnsTheOriginalCachedObject() - //{ - // Func fetch = () => 123; - // CacheEntryRemovedArguments removedCallbackArgs = null; - // CacheEntryRemovedCallback callback = args => removedCallbackArgs = args; - - - // sut.GetOrAdd(TestKey, fetch, - // new MemoryCacheEntryOptions - // { - // AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100), - // RemovedCallback = callback - // }); - // sut.Get(TestKey); + [Test] + [MaxTime(20000)] + public void GetOrAddWithPostEvictionCallbackdReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand() + { + object cacheValue = null; + var cacheEntryOptions = new MemoryCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100) + }.RegisterPostEvictionCallback((key, value, reason, state) => cacheValue = value); + sut.GetOrAdd(TestKey, () => 123, + cacheEntryOptions); - // sut.Remove(TestKey); //force removed callback to fire - // while (removedCallbackArgs == null) - // Thread.Sleep(500); + sut.Remove(TestKey); //force removed callback to fire + while (cacheValue == null) + Thread.Sleep(500); - // Assert.AreEqual(123, removedCallbackArgs.CacheItem.Value); - //} + cacheValue.Should().Be(123); + } [Test] public void GetWithClassTypeParamReturnsType() From e96a25838b150dfbf44cd03ba59a91207663af15 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 22:46:41 +0000 Subject: [PATCH 18/39] feat: version lazycache and lazycache.aspnetcore using env variables --- LazyCache.AspNetCore/LazyCache.AspNetCore.csproj | 8 +++++++- LazyCache/LazyCache.csproj | 8 +++++++- appveyor.yml | 16 +++++++--------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj index 28b24c5..2b21f8b 100644 --- a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj +++ b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj @@ -1,10 +1,16 @@ + netstandard2.0 LazyCache true - 2.0.0 + 1.0.0 + + $(LazyCacheAspNetCoreVersion)$(LazyCacheAspNetCoreVersionSuffix) + $(APPVEYOR_BUILD_NUMBER) + 0 + $(LazyCacheAspNetCoreVersion).$(AppVeyorBuildNumber) https://github.com/alastairtree https://github.com/alastairtree ServiceCollection regististrations fopr LazyCache to initialise the depndency injection diff --git a/LazyCache/LazyCache.csproj b/LazyCache/LazyCache.csproj index 397df86..e682f60 100644 --- a/LazyCache/LazyCache.csproj +++ b/LazyCache/LazyCache.csproj @@ -1,9 +1,15 @@ + netstandard2.0 true - 2.0.0 + 1.0.0 + + $(LazyCacheVersion)$(LazyCacheVersionSuffix) + $(APPVEYOR_BUILD_NUMBER) + 0 + $(LazyCacheVersion).$(AppVeyorBuildNumber) https://github.com/alastairtree https://github.com/alastairtree Lazy cache is a simple, thread safe in-memory caching service diff --git a/appveyor.yml b/appveyor.yml index ae170ee..df1fef4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,11 @@ -version: 2.0.0.{build}-beta01 +version: '{LazyCacheVersion}.{build}{LazyCacheVersionSuffix}' +image: Visual Studio 2017 configuration: Release -dotnet_csproj: - patch: true - file: '**\*.csproj' - version: '{version}' - package_version: '{version}' - assembly_version: '{version}' - file_version: '{version}' - informational_version: '{version}' +environment: + LazyCacheVersion: 2.0.0 + LazyCacheVersionSuffix: -beta01 + LazyCacheAspNetCoreVersion: 2.0.0 + LazyCacheAspNetCoreVersionSuffix: -beta01 build_script: - ps: '& .\build.ps1' deploy: off \ No newline at end of file From 727d8ce7925a0fd03036925fd1d54b14fcb9d228 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 22:51:23 +0000 Subject: [PATCH 19/39] bug: ignoring suffix --- LazyCache.AspNetCore/LazyCache.AspNetCore.csproj | 2 +- LazyCache/LazyCache.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj index 2b21f8b..1642c6e 100644 --- a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj +++ b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj @@ -6,7 +6,7 @@ LazyCache true 1.0.0 - + $(LazyCacheAspNetCoreVersion)$(LazyCacheAspNetCoreVersionSuffix) $(APPVEYOR_BUILD_NUMBER) 0 diff --git a/LazyCache/LazyCache.csproj b/LazyCache/LazyCache.csproj index e682f60..9ce628c 100644 --- a/LazyCache/LazyCache.csproj +++ b/LazyCache/LazyCache.csproj @@ -5,7 +5,7 @@ netstandard2.0 true 1.0.0 - + $(LazyCacheVersion)$(LazyCacheVersionSuffix) $(APPVEYOR_BUILD_NUMBER) 0 From 98bbc894b3e469e159d4392a77519fb35e9f5591 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 22:55:14 +0000 Subject: [PATCH 20/39] bug: fix version name --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index df1fef4..6580d87 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: '{LazyCacheVersion}.{build}{LazyCacheVersionSuffix}' +version: '2.0.0.{build}' image: Visual Studio 2017 configuration: Release environment: From d0d49f925ab3d7a13289eae1061c06991828f266 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 3 Mar 2018 23:26:15 +0000 Subject: [PATCH 21/39] feat: tweak DI setup to include memory cache dependency --- .../LazyCache.AspNetCore.csproj | 2 + .../LazyCacheServiceCollectionExtensions.cs | 40 +++++++++++++++++++ .../ServiceCollectionExtensions.cs | 28 ------------- 3 files changed, 42 insertions(+), 28 deletions(-) create mode 100644 LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs delete mode 100644 LazyCache.AspNetCore/ServiceCollectionExtensions.cs diff --git a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj index 1642c6e..a195143 100644 --- a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj +++ b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj @@ -24,6 +24,8 @@ + + diff --git a/LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs b/LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs new file mode 100644 index 0000000..ed1b6e9 --- /dev/null +++ b/LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs @@ -0,0 +1,40 @@ +using System; +using LazyCache; +using LazyCache.Providers; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection.Extensions; + +// ReSharper disable once CheckNamespace - MS guidelines say put DI registration in this NS +namespace Microsoft.Extensions.DependencyInjection +{ + // See https://github.com/aspnet/Caching/blob/dev/src/Microsoft.Extensions.Caching.Memory/MemoryCacheServiceCollectionExtensions.cs + public static class LazyCacheServiceCollectionExtensions + { + public static IServiceCollection AddLazyCache(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + services.AddOptions(); + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); + + services.TryAdd(ServiceDescriptor.Singleton()); + + return services; + } + + public static IServiceCollection AddLazyCache(this IServiceCollection services, Func implmentationFactory) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + if (implmentationFactory == null) throw new ArgumentNullException(nameof(implmentationFactory)); + + services.AddOptions(); + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); + + services.TryAdd(ServiceDescriptor.Singleton(implmentationFactory)); + + return services; + } + } +} diff --git a/LazyCache.AspNetCore/ServiceCollectionExtensions.cs b/LazyCache.AspNetCore/ServiceCollectionExtensions.cs deleted file mode 100644 index 86bd703..0000000 --- a/LazyCache.AspNetCore/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using LazyCache; - -// ReSharper disable once CheckNamespace - MS guidelines say put DI registration in this NS -namespace Microsoft.Extensions.DependencyInjection -{ - public static class LazyCacheServiceRegistration - { - public static IServiceCollection AddLazyCache(this IServiceCollection services) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - services.AddSingleton(); - - return services; - } - - public static IServiceCollection AddLazyCache(this IServiceCollection services, Func implmentationFactory) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - if (implmentationFactory == null) throw new ArgumentNullException(nameof(implmentationFactory)); - - services.AddSingleton(implmentationFactory); - - return services; - } - } -} From ce43ad1c7c20413d89743407b1a0d9ee12fba9d4 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 4 Mar 2018 00:07:51 +0000 Subject: [PATCH 22/39] feat: more polish of the async/await and .ConfigureAwait(false) --- LazyCache/CachingService.cs | 41 +++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 76f3481..0477e6f 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -87,13 +87,13 @@ public virtual T Get(string key) return UnwrapLazy(item); } - public virtual async Task GetAsync(string key) + public virtual Task GetAsync(string key) { ValidateKey(key); var item = CacheProvider.Get(key); - return await UnwrapAsyncLazys(item); + return UnwrapAsyncLazys(item); } public virtual T GetOrAdd(string key, Func addItemFactory) @@ -106,8 +106,9 @@ public virtual async Task GetOrAddAsync(string key, Func> addItemF { ValidateKey(key); - EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(policy); + EnsureEvictionCallbackDoesNotReturnTheAsyncLazy(policy); + //any other way of doing this? //var cacheItem = CacheProvider.GetOrCreateAsync(key, async entry => // await new AsyncLazy(async () => { // entry.SetOptions(policy); @@ -122,7 +123,7 @@ public virtual async Task GetOrAddAsync(string key, Func> addItemF //}); object cacheItem; - await locker.WaitAsync(); //TODO: do we really need this? + await locker.WaitAsync().ConfigureAwait(false); //TODO: do we really need to lock - or can we lock just the key? try { cacheItem = CacheProvider.GetOrCreate(key, entry => @@ -145,7 +146,7 @@ public virtual async Task GetOrAddAsync(string key, Func> addItemF if (result.IsCanceled || result.IsFaulted) CacheProvider.Remove(key); - return await result; + return await result.ConfigureAwait(false); } catch //addItemFactory errored so do not cache the exception { @@ -168,7 +169,7 @@ public virtual T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntr { ValidateKey(key); - EnsureRemovedCallbackDoesNotReturnTheLazy(policy); + EnsureEvictionCallbackDoesNotReturnTheLazy(policy); object cacheItem; locker.Wait(); //TODO: do we really need this? @@ -206,20 +207,20 @@ 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, DefaultExpiryDateTime); + return GetOrAddAsync(key, addItemFactory, DefaultExpiryDateTime); } - public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires) + public virtual Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires) { - return await GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); + return GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); } - public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, + public virtual Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration) { - return await GetOrAddAsync(key, addItemFactory, + return GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } @@ -240,24 +241,24 @@ protected virtual T UnwrapLazy(object item) return default(T); } - protected virtual async Task UnwrapAsyncLazys(object item) + protected virtual Task UnwrapAsyncLazys(object item) { if (item is AsyncLazy asyncLazy) - return await asyncLazy.Value; + return asyncLazy.Value; if (item is Task task) - return await task; + return task; if (item is Lazy lazy) - return lazy.Value; + return Task.FromResult(lazy.Value); if (item is T variable) - return variable; + return Task.FromResult(variable); - return default(T); + return Task.FromResult(default(T)); } - protected virtual void EnsureRemovedCallbackDoesNotReturnTheLazy(MemoryCacheEntryOptions policy) + protected virtual void EnsureEvictionCallbackDoesNotReturnTheLazy(MemoryCacheEntryOptions policy) { if (policy?.PostEvictionCallbacks != null) foreach (var item in policy.PostEvictionCallbacks) @@ -274,7 +275,7 @@ protected virtual void EnsureRemovedCallbackDoesNotReturnTheLazy(MemoryCacheE } } - protected virtual void EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(MemoryCacheEntryOptions policy) + protected virtual void EnsureEvictionCallbackDoesNotReturnTheAsyncLazy(MemoryCacheEntryOptions policy) { if (policy?.PostEvictionCallbacks != null) foreach (var item in policy.PostEvictionCallbacks) From 1d71fc032b9e28ffdcf34e829583f232517fcbd2 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 4 Mar 2018 00:32:21 +0000 Subject: [PATCH 23/39] feat: add CacheItemPolicy for backwards compat --- LazyCache/CacheItemPolicy.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 LazyCache/CacheItemPolicy.cs diff --git a/LazyCache/CacheItemPolicy.cs b/LazyCache/CacheItemPolicy.cs new file mode 100644 index 0000000..d89282d --- /dev/null +++ b/LazyCache/CacheItemPolicy.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq; +using Microsoft.Extensions.Caching.Memory; + +namespace LazyCache +{ + [Obsolete( + "CacheItemPolicy was part of System.Runtime.Caching which is no longer used by LazyCache. " + + "This class is a fake used to maintain backward compatibility and will be removed in a later version."+ + "Change to MemoryCacheEntryOptions instead")] + public class CacheItemPolicy : MemoryCacheEntryOptions + { + public PostEvictionCallbackRegistration RemovedCallback => PostEvictionCallbacks.FirstOrDefault(); + } +} \ No newline at end of file From 64a5bac1c56450e109d586e2563de7ead36a358c Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 4 Mar 2018 00:32:40 +0000 Subject: [PATCH 24/39] feat: dispose MemoryCache --- LazyCache/ICacheProvider.cs | 2 +- LazyCache/Mocks/MockCachingService.cs | 4 ++++ LazyCache/Providers/MemoryCacheProvider.cs | 7 ++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/LazyCache/ICacheProvider.cs b/LazyCache/ICacheProvider.cs index 9b73220..2007416 100644 --- a/LazyCache/ICacheProvider.cs +++ b/LazyCache/ICacheProvider.cs @@ -4,7 +4,7 @@ namespace LazyCache { - public interface ICacheProvider + public interface ICacheProvider : IDisposable { void Set(string key, object item, MemoryCacheEntryOptions policy); object Get(string key); diff --git a/LazyCache/Mocks/MockCachingService.cs b/LazyCache/Mocks/MockCachingService.cs index 4b12395..a75f905 100644 --- a/LazyCache/Mocks/MockCachingService.cs +++ b/LazyCache/Mocks/MockCachingService.cs @@ -108,5 +108,9 @@ public Task GetOrCreateAsync(string key, Func> func) { return func(null); } + + public void Dispose() + { + } } } \ No newline at end of file diff --git a/LazyCache/Providers/MemoryCacheProvider.cs b/LazyCache/Providers/MemoryCacheProvider.cs index c85f116..a2e9396 100644 --- a/LazyCache/Providers/MemoryCacheProvider.cs +++ b/LazyCache/Providers/MemoryCacheProvider.cs @@ -6,7 +6,7 @@ namespace LazyCache.Providers { public class MemoryCacheProvider : ICacheProvider { - private readonly IMemoryCache cache; + internal readonly IMemoryCache cache; public MemoryCacheProvider() : this(new MemoryCache(new MemoryCacheOptions())) { @@ -41,5 +41,10 @@ public Task GetOrCreateAsync(string key, Func> factor { return cache.GetOrCreateAsync(key, factory); } + + public void Dispose() + { + cache?.Dispose(); + } } } \ No newline at end of file From 36c58c5196734cdc9a5e783dff0ffe3f96df5210 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 4 Mar 2018 00:32:56 +0000 Subject: [PATCH 25/39] task: release notes --- ReleaseNotes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index d32c759..19bd663 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -4,7 +4,7 @@ - *BREAKING CHANGE* Upgrade to netstandard2.0 - *BREAKING CHANGE* Change underlying cache from System.Runtime.Caching to Microsft.Extension.Caching.Memory - *BREAKING CHANGE* Removed IAppCache.ObjectCache and changed to a cache provider model. - To access the provider use IAppCache.CacheProvider. By default we use a static shared in-memory cache but add your own cache provider by implmenting the simple `ICacheProvider`. + To access the provider use IAppCache.CacheProvider. By default we use a singleton shared in-memory cache but add your own cache provider by implmenting the simple `ICacheProvider`. - *BREAKING CHANGE* changed from CacheItemPolicy to MemoryCacheEntryOptions. RemovedCallback is now PostEvictionCallbacks. - Added a new replaceable global static default cache provider @@ -12,6 +12,8 @@ By default we use a shared in-memory cache but each instance can have it's underlying cache provider overridden from it's constructor. - Make methods on CachingService virtual +- Add LazyCache.AspNetCore for dependency injection registration - ServiceCollection.AddLazyCache(); +- Update sample to use aspnet core and LazyCache.AspNetCore ## Version 0.7.1 - Fix async/sync interopability bug, see https://github.com/alastairtree/LazyCache/issues/12 From 187fee4f185f2f76f4647ccde8246234b49c87e8 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 4 Mar 2018 12:35:18 +0000 Subject: [PATCH 26/39] chore: small refactor --- LazyCache/CachingService.cs | 119 ++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 60 deletions(-) diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 0477e6f..33c2b00 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -33,16 +33,16 @@ public CachingService(ICacheProvider cache) : this(() => cache) if (cache == null) throw new ArgumentNullException(nameof(cache)); } - public static Lazy DefaultCacheProvider { get; set; } + public static Lazy DefaultCacheProvider { get; set; } = new Lazy(() => new MemoryCacheProvider()); /// - /// Seconds to cache objects for by default + /// Seconds to cache objects for by default /// public virtual int DefaultCacheDurationSeconds { get; set; } = 60 * 20; /// - /// Seconds to cache objects for by default + /// Seconds to cache objects for by default /// [Obsolete("DefaultCacheDuration has been replaced with DefaultCacheDurationSeconds")] @@ -101,60 +101,6 @@ public virtual T GetOrAdd(string key, Func addItemFactory) return GetOrAdd(key, addItemFactory, DefaultExpiryDateTime); } - public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, - MemoryCacheEntryOptions policy) - { - ValidateKey(key); - - EnsureEvictionCallbackDoesNotReturnTheAsyncLazy(policy); - - //any other way of doing this? - //var cacheItem = CacheProvider.GetOrCreateAsync(key, async entry => - // await new AsyncLazy(async () => { - // entry.SetOptions(policy); - // return await addItemFactory.Invoke(); - // } - //).Value); - - //var cacheItem = CacheProvider.GetOrCreateAsync(key, async entry => - //{ - // entry.SetOptions(policy); - // return new AsyncLazy(async () => await addItemFactory.Invoke()); - //}); - - object cacheItem; - await locker.WaitAsync().ConfigureAwait(false); //TODO: do we really need to lock - or can we lock just the key? - try - { - cacheItem = CacheProvider.GetOrCreate(key, entry => - { - entry.SetOptions(policy); - var value = new AsyncLazy(addItemFactory); - return (object) value; - } - ); - } - finally - { - locker.Release(); - } - - try - { - var result = UnwrapAsyncLazys(cacheItem); - - if (result.IsCanceled || result.IsFaulted) - CacheProvider.Remove(key); - - return await result.ConfigureAwait(false); - } - catch //addItemFactory errored so do not cache the exception - { - CacheProvider.Remove(key); - throw; - } - } - public virtual T GetOrAdd(string key, Func addItemFactory, DateTimeOffset expires) { return GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); @@ -175,11 +121,10 @@ public virtual T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntr locker.Wait(); //TODO: do we really need this? try { - cacheItem = CacheProvider.GetOrCreate(key, entry => + cacheItem = CacheProvider.GetOrCreate(key, entry => { entry.SetOptions(policy); - var value = new Lazy(addItemFactory); - return (object) value; + return new Lazy(addItemFactory); } ); } @@ -224,6 +169,60 @@ public virtual Task GetOrAddAsync(string key, Func> addItemFactory new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, + MemoryCacheEntryOptions policy) + { + ValidateKey(key); + + EnsureEvictionCallbackDoesNotReturnTheAsyncLazy(policy); + + //any other way of doing this? + //var cacheItem = CacheProvider.GetOrCreateAsync(key, async entry => + // await new AsyncLazy(async () => { + // entry.SetOptions(policy); + // return await addItemFactory.Invoke(); + // } + //).Value); + + //var cacheItem = CacheProvider.GetOrCreateAsync(key, async entry => + //{ + // entry.SetOptions(policy); + // return new AsyncLazy(async () => await addItemFactory.Invoke()); + //}); + + object cacheItem; + await locker.WaitAsync() + .ConfigureAwait(false); //TODO: do we really need to lock - or can we lock just the key? + try + { + cacheItem = CacheProvider.GetOrCreate(key, entry => + { + entry.SetOptions(policy); + return new AsyncLazy(addItemFactory); + } + ); + } + finally + { + locker.Release(); + } + + try + { + var result = UnwrapAsyncLazys(cacheItem); + + if (result.IsCanceled || result.IsFaulted) + CacheProvider.Remove(key); + + return await result.ConfigureAwait(false); + } + catch //addItemFactory errored so do not cache the exception + { + CacheProvider.Remove(key); + throw; + } + } + protected virtual T UnwrapLazy(object item) { if (item is Lazy lazy) From 51b766fd85a6101f0e767a05bc5d9e93eae59bfc Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 4 Mar 2018 13:24:39 +0000 Subject: [PATCH 27/39] feat: Add GetOrAdd with Func> Allows the user to specify the cache options when the factory is executed. Requested in #5 --- .../CachingServiceMemoryCacheProviderTests.cs | 1 + LazyCache/CachingService.cs | 75 ++++++++++--------- LazyCache/IAppCache.cs | 4 + 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs index 028ea53..99e2471 100644 --- a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs +++ b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs @@ -671,6 +671,7 @@ public void GetOrAddWithPostEvictionCallbackdReturnsTheOriginalCachedObjectEvenI while (cacheValue == null) Thread.Sleep(500); + cacheValue.Should().BeOfType(); cacheValue.Should().Be(123); } diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 33c2b00..744f9b4 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using LazyCache.Providers; @@ -113,19 +114,28 @@ public virtual T GetOrAdd(string key, Func addItemFactory, TimeSpan slidin public virtual T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy) { - ValidateKey(key); + return GetOrAdd(key, entry => + { + entry.SetOptions(policy); + return addItemFactory(); + }); + } - EnsureEvictionCallbackDoesNotReturnTheLazy(policy); + public virtual T GetOrAdd(string key, Func addItemFactory) + { + ValidateKey(key); object cacheItem; - locker.Wait(); //TODO: do we really need this? + locker.Wait(); //TODO: do we really need this? Could we just lock on the key? try { cacheItem = CacheProvider.GetOrCreate(key, entry => + new Lazy(() => { - entry.SetOptions(policy); - return new Lazy(addItemFactory); - } + var result = addItemFactory(entry); + EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy(entry.PostEvictionCallbacks); + return result; + }) ); } finally @@ -169,12 +179,19 @@ public virtual Task GetOrAddAsync(string key, Func> addItemFactory new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); } - public virtual async Task GetOrAddAsync(string key, Func> addItemFactory, + public virtual Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy) { - ValidateKey(key); + return GetOrAddAsync(key, entry => + { + entry.SetOptions(policy); + return addItemFactory(); + }); + } - EnsureEvictionCallbackDoesNotReturnTheAsyncLazy(policy); + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory) + { + ValidateKey(key); //any other way of doing this? //var cacheItem = CacheProvider.GetOrCreateAsync(key, async entry => @@ -196,10 +213,12 @@ await locker.WaitAsync() try { cacheItem = CacheProvider.GetOrCreate(key, entry => + new AsyncLazy(() => { - entry.SetOptions(policy); - return new AsyncLazy(addItemFactory); - } + var result = addItemFactory(entry); + EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy(entry.PostEvictionCallbacks); + return result; + }) ); } finally @@ -257,34 +276,22 @@ protected virtual Task UnwrapAsyncLazys(object item) return Task.FromResult(default(T)); } - protected virtual void EnsureEvictionCallbackDoesNotReturnTheLazy(MemoryCacheEntryOptions policy) + protected virtual void EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy( + IList callbackRegistrations) { - if (policy?.PostEvictionCallbacks != null) - foreach (var item in policy.PostEvictionCallbacks) + if (callbackRegistrations != null) + foreach (var item in callbackRegistrations) { - var originallCallback = item.EvictionCallback; + var originalCallback = item.EvictionCallback; item.EvictionCallback = (key, value, reason, state) => { - //unwrap the cache item in a callback given one is specified - if (value is Lazy cacheItem) + // before the original callback we need to unwrap the Lazy that holds the cache item + if (value is AsyncLazy asyncCacheItem) + value = asyncCacheItem.IsValueCreated ? asyncCacheItem.Value : Task.FromResult(default(T)); + else if (value is Lazy cacheItem) value = cacheItem.IsValueCreated ? cacheItem.Value : default(T); - ; - originallCallback(key, value, reason, state); - }; - } - } - protected virtual void EnsureEvictionCallbackDoesNotReturnTheAsyncLazy(MemoryCacheEntryOptions policy) - { - if (policy?.PostEvictionCallbacks != null) - foreach (var item in policy.PostEvictionCallbacks) - { - var originalCallback = item.EvictionCallback; - item.EvictionCallback = (key, value, reason, state) => - { - //unwrap the cache item in a callback given one is specified - if (value is AsyncLazy cacheItem) - value = cacheItem.IsValueCreated ? cacheItem.Value : Task.FromResult(default(T)); + // pass the unwrapped cached value to the original callback originalCallback(key, value, reason, state); }; } diff --git a/LazyCache/IAppCache.cs b/LazyCache/IAppCache.cs index 1128918..14ec6ab 100644 --- a/LazyCache/IAppCache.cs +++ b/LazyCache/IAppCache.cs @@ -7,6 +7,7 @@ namespace LazyCache public interface IAppCache { ICacheProvider CacheProvider { get; } + void Add(string key, T item); void Add(string key, T item, DateTimeOffset absoluteExpiration); void Add(string key, T item, TimeSpan slidingExpiration); @@ -18,6 +19,7 @@ public interface IAppCache T GetOrAdd(string key, Func addItemFactory, DateTimeOffset absoluteExpiration); T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpiration); T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy); + T GetOrAdd(string key, Func addItemFactory); void Remove(string key); @@ -25,6 +27,8 @@ public interface IAppCache Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy); Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires); Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration); + Task GetOrAddAsync(string key, Func> addItemFactory); + Task GetAsync(string key); } } \ No newline at end of file From b0b970ee855a2396be0fb4ae60ab9aeea74d9f8e Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 4 Mar 2018 15:27:15 +0000 Subject: [PATCH 28/39] feat: move app cache overload methods out to extensions methods --- CacheDatabaseQueriesApiSample/Startup.cs | 4 +- .../LazyCacheServiceCollectionExtensions.cs | 5 +- .../CachingServiceMemoryCacheProviderTests.cs | 14 ++- LazyCache/AppCacheExtenions.cs | 100 ++++++++++++++++++ LazyCache/CacheDefaults.cs | 18 ++++ LazyCache/CacheItemPolicy.cs | 2 +- LazyCache/CachingService.cs | 82 ++------------ LazyCache/IAppCache.cs | 20 ++-- LazyCache/Mocks/MockCacheEntry.cs | 29 +++++ LazyCache/Mocks/MockCacheProvider.cs | 36 +++++++ LazyCache/Mocks/MockCachingService.cs | 82 +------------- ReleaseNotes.md | 10 +- 12 files changed, 227 insertions(+), 175 deletions(-) create mode 100644 LazyCache/AppCacheExtenions.cs create mode 100644 LazyCache/CacheDefaults.cs create mode 100644 LazyCache/Mocks/MockCacheEntry.cs create mode 100644 LazyCache/Mocks/MockCacheProvider.cs diff --git a/CacheDatabaseQueriesApiSample/Startup.cs b/CacheDatabaseQueriesApiSample/Startup.cs index 7d891be..e1a3044 100644 --- a/CacheDatabaseQueriesApiSample/Startup.cs +++ b/CacheDatabaseQueriesApiSample/Startup.cs @@ -1,5 +1,4 @@ -using LazyCache; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -30,7 +29,6 @@ public void ConfigureServices(IServiceCollection services) // Register IAppCache as a singleton CachingService services.AddLazyCache(); - } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs b/LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs index ed1b6e9..3a5643e 100644 --- a/LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs +++ b/LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs @@ -23,7 +23,8 @@ public static IServiceCollection AddLazyCache(this IServiceCollection services) return services; } - public static IServiceCollection AddLazyCache(this IServiceCollection services, Func implmentationFactory) + public static IServiceCollection AddLazyCache(this IServiceCollection services, + Func implmentationFactory) { if (services == null) throw new ArgumentNullException(nameof(services)); if (implmentationFactory == null) throw new ArgumentNullException(nameof(implmentationFactory)); @@ -37,4 +38,4 @@ public static IServiceCollection AddLazyCache(this IServiceCollection services, return services; } } -} +} \ No newline at end of file diff --git a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs index 99e2471..8293fab 100644 --- a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs +++ b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs @@ -24,7 +24,7 @@ private static CachingService BuildCache() return new CachingService(new MemoryCacheProvider()); } - private CachingService sut; + private IAppCache sut; private readonly MemoryCacheEntryOptions oneHourNonRemoveableMemoryCacheEntryOptions = new MemoryCacheEntryOptions @@ -175,6 +175,12 @@ public void AddWithSlidingThatExpiresReturnsNull() Assert.IsNull(sut.Get(TestKey)); } + [Test] + public void CacheProviderIsNotNull() + { + sut.CacheProvider.Should().NotBeNull(); + } + [Test] public void DefaultContructorThenGetOrAddFromSecondCachingServiceHasSharedUnderlyingCache() { @@ -626,7 +632,11 @@ public void GetOrAddWithPolicyAndThenGetObjectReturnsCorrectType() [Test] public void GetOrAddWithPolicyAndThenGetValueObjectReturnsCorrectType() { - int Fetch() => 123; + int Fetch() + { + return 123; + } + sut.GetOrAdd(TestKey, Fetch, oneHourNonRemoveableMemoryCacheEntryOptions); var actual = sut.Get(TestKey); Assert.AreEqual(123, actual); diff --git a/LazyCache/AppCacheExtenions.cs b/LazyCache/AppCacheExtenions.cs new file mode 100644 index 0000000..3823a87 --- /dev/null +++ b/LazyCache/AppCacheExtenions.cs @@ -0,0 +1,100 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; + +namespace LazyCache +{ + public static class AppCacheExtenions + { + 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()); + } + + 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}); + } + + 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}); + } + + public static T GetOrAdd(this IAppCache cache, string key, Func addItemFactory) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAdd(key, addItemFactory, cache.DefaultCachePolicy.BuildOptions()); + } + + public static T GetOrAdd(this IAppCache cache, string key, Func addItemFactory, DateTimeOffset expires) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); + } + + public static T GetOrAdd(this IAppCache cache, string key, Func addItemFactory, + TimeSpan slidingExpiration) + { + return cache.GetOrAdd(key, addItemFactory, + new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); + } + + public static T GetOrAdd(this IAppCache cache, string key, Func addItemFactory, + MemoryCacheEntryOptions policy) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAdd(key, entry => + { + entry.SetOptions(policy); + return addItemFactory(); + }); + } + + public static Task GetOrAddAsync(this IAppCache cache, string key, Func> addItemFactory) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAddAsync(key, addItemFactory, cache.DefaultCachePolicy.BuildOptions()); + } + + + public static Task GetOrAddAsync(this IAppCache cache, string key, Func> addItemFactory, + DateTimeOffset expires) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); + } + + public static Task GetOrAddAsync(this IAppCache cache, string key, Func> addItemFactory, + TimeSpan slidingExpiration) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAddAsync(key, addItemFactory, + new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); + } + + public static Task GetOrAddAsync(this IAppCache cache, string key, Func> addItemFactory, + MemoryCacheEntryOptions policy) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAddAsync(key, entry => + { + entry.SetOptions(policy); + return addItemFactory(); + }); + } + } +} \ No newline at end of file diff --git a/LazyCache/CacheDefaults.cs b/LazyCache/CacheDefaults.cs new file mode 100644 index 0000000..d76096f --- /dev/null +++ b/LazyCache/CacheDefaults.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Extensions.Caching.Memory; + +namespace LazyCache +{ + public class CacheDefaults + { + public virtual int DefaultCacheDurationSeconds { get; set; } = 60 * 20; + + internal MemoryCacheEntryOptions BuildOptions() + { + return new MemoryCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(DefaultCacheDurationSeconds) + }; + } + } +} \ No newline at end of file diff --git a/LazyCache/CacheItemPolicy.cs b/LazyCache/CacheItemPolicy.cs index d89282d..be940fc 100644 --- a/LazyCache/CacheItemPolicy.cs +++ b/LazyCache/CacheItemPolicy.cs @@ -6,7 +6,7 @@ namespace LazyCache { [Obsolete( "CacheItemPolicy was part of System.Runtime.Caching which is no longer used by LazyCache. " + - "This class is a fake used to maintain backward compatibility and will be removed in a later version."+ + "This class is a fake used to maintain backward compatibility and will be removed in a later version." + "Change to MemoryCacheEntryOptions instead")] public class CacheItemPolicy : MemoryCacheEntryOptions { diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 744f9b4..76487bc 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -22,7 +22,6 @@ public CachingService(Lazy cacheProvider) this.cacheProvider = cacheProvider ?? throw new ArgumentNullException(nameof(cacheProvider)); } - public CachingService(Func cacheProviderFactory) { if (cacheProviderFactory == null) throw new ArgumentNullException(nameof(cacheProviderFactory)); @@ -37,38 +36,20 @@ public CachingService(ICacheProvider cache) : this(() => cache) public static Lazy DefaultCacheProvider { get; set; } = new Lazy(() => new MemoryCacheProvider()); - /// - /// Seconds to cache objects for by default - /// - public virtual int DefaultCacheDurationSeconds { get; set; } = 60 * 20; - /// /// Seconds to cache objects for by default /// [Obsolete("DefaultCacheDuration has been replaced with DefaultCacheDurationSeconds")] - public virtual int DefaultCacheDuration { - get => DefaultCacheDurationSeconds; - set => DefaultCacheDurationSeconds = value; - } - - private DateTimeOffset DefaultExpiryDateTime => DateTimeOffset.Now.AddSeconds(DefaultCacheDurationSeconds); - - public virtual void Add(string key, T item) - { - Add(key, item, DefaultExpiryDateTime); - } - - public virtual void Add(string key, T item, DateTimeOffset expires) - { - Add(key, item, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); + get => DefaultCachePolicy.DefaultCacheDurationSeconds; + set => DefaultCachePolicy.DefaultCacheDurationSeconds = value; } - public virtual void Add(string key, T item, TimeSpan slidingExpiration) - { - Add(key, item, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); - } + /// + /// Policy defining how long items should be cached for unless specified + /// + public virtual CacheDefaults DefaultCachePolicy { get; set; } = new CacheDefaults(); public virtual void Add(string key, T item, MemoryCacheEntryOptions policy) { @@ -97,30 +78,6 @@ public virtual Task GetAsync(string key) return UnwrapAsyncLazys(item); } - public virtual T GetOrAdd(string key, Func addItemFactory) - { - return GetOrAdd(key, addItemFactory, DefaultExpiryDateTime); - } - - public virtual T GetOrAdd(string key, Func addItemFactory, DateTimeOffset expires) - { - return GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); - } - - public virtual T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpiration) - { - return GetOrAdd(key, addItemFactory, new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); - } - - public virtual T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy) - { - return GetOrAdd(key, entry => - { - entry.SetOptions(policy); - return addItemFactory(); - }); - } - public virtual T GetOrAdd(string key, Func addItemFactory) { ValidateKey(key); @@ -162,33 +119,6 @@ public virtual void Remove(string key) public virtual ICacheProvider CacheProvider => cacheProvider.Value; - public virtual Task GetOrAddAsync(string key, Func> addItemFactory) - { - return GetOrAddAsync(key, addItemFactory, DefaultExpiryDateTime); - } - - public virtual Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires) - { - return GetOrAddAsync(key, addItemFactory, new MemoryCacheEntryOptions {AbsoluteExpiration = expires}); - } - - public virtual Task GetOrAddAsync(string key, Func> addItemFactory, - TimeSpan slidingExpiration) - { - return GetOrAddAsync(key, addItemFactory, - new MemoryCacheEntryOptions {SlidingExpiration = slidingExpiration}); - } - - public virtual Task GetOrAddAsync(string key, Func> addItemFactory, - MemoryCacheEntryOptions policy) - { - return GetOrAddAsync(key, entry => - { - entry.SetOptions(policy); - return addItemFactory(); - }); - } - public virtual async Task GetOrAddAsync(string key, Func> addItemFactory) { ValidateKey(key); diff --git a/LazyCache/IAppCache.cs b/LazyCache/IAppCache.cs index 14ec6ab..b474cbe 100644 --- a/LazyCache/IAppCache.cs +++ b/LazyCache/IAppCache.cs @@ -8,27 +8,21 @@ public interface IAppCache { ICacheProvider CacheProvider { get; } - void Add(string key, T item); - void Add(string key, T item, DateTimeOffset absoluteExpiration); - void Add(string key, T item, TimeSpan slidingExpiration); + /// + /// Define the number of seconds to cache objects for by default + /// + CacheDefaults DefaultCachePolicy { get; } + void Add(string key, T item, MemoryCacheEntryOptions policy); T Get(string key); - T GetOrAdd(string key, Func addItemFactory); - T GetOrAdd(string key, Func addItemFactory, DateTimeOffset absoluteExpiration); - T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpiration); - T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy); T GetOrAdd(string key, Func addItemFactory); - void Remove(string key); + Task GetAsync(string key); - Task GetOrAddAsync(string key, Func> addItemFactory); - Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy); - Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires); - Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration); Task GetOrAddAsync(string key, Func> addItemFactory); - Task GetAsync(string key); + void Remove(string key); } } \ No newline at end of file diff --git a/LazyCache/Mocks/MockCacheEntry.cs b/LazyCache/Mocks/MockCacheEntry.cs new file mode 100644 index 0000000..c306ca0 --- /dev/null +++ b/LazyCache/Mocks/MockCacheEntry.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Primitives; + +namespace LazyCache.Mocks +{ + public class MockCacheEntry : ICacheEntry + { + public MockCacheEntry(string key) + { + Key = key; + } + + public void Dispose() + { + } + + public object Key { get; } + public object Value { get; set; } + public DateTimeOffset? AbsoluteExpiration { get; set; } + public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; } + public TimeSpan? SlidingExpiration { get; set; } + public IList ExpirationTokens { get; } + public IList PostEvictionCallbacks { get; } + public CacheItemPriority Priority { get; set; } + public long? Size { get; set; } + } +} \ No newline at end of file diff --git a/LazyCache/Mocks/MockCacheProvider.cs b/LazyCache/Mocks/MockCacheProvider.cs new file mode 100644 index 0000000..c7463e7 --- /dev/null +++ b/LazyCache/Mocks/MockCacheProvider.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; + +namespace LazyCache.Mocks +{ + public class MockCacheProvider : ICacheProvider + { + public void Set(string key, object item, MemoryCacheEntryOptions policy) + { + } + + public object Get(string key) + { + return null; + } + + public object GetOrCreate(string key, Func func) + { + return func(null); + } + + public void Remove(string key) + { + } + + public Task GetOrCreateAsync(string key, Func> func) + { + return func(null); + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/LazyCache/Mocks/MockCachingService.cs b/LazyCache/Mocks/MockCachingService.cs index a75f905..af7be48 100644 --- a/LazyCache/Mocks/MockCachingService.cs +++ b/LazyCache/Mocks/MockCachingService.cs @@ -11,52 +11,25 @@ namespace LazyCache.Mocks public class MockCachingService : IAppCache { public ICacheProvider CacheProvider { get; } = new MockCacheProvider(); - - public void Add(string key, T item) - { - } - - public void Add(string key, T item, DateTimeOffset expires) - { - } + public CacheDefaults DefaultCachePolicy { get; set; } public T Get(string key) { return default(T); } - public T GetOrAdd(string key, Func addItemFactory) + public T GetOrAdd(string key, Func addItemFactory) { - return addItemFactory.Invoke(); - } - - public T GetOrAdd(string key, Func addItemFactory, DateTimeOffset expires) - { - return addItemFactory.Invoke(); + return addItemFactory(new MockCacheEntry(key)); } public void Remove(string key) { } - public Task GetOrAddAsync(string key, Func> addItemFactory, MemoryCacheEntryOptions policy) - { - return addItemFactory.Invoke(); - } - - public Task GetOrAddAsync(string key, Func> addItemFactory) + public Task GetOrAddAsync(string key, Func> addItemFactory) { - return addItemFactory.Invoke(); - } - - public Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires) - { - return addItemFactory.Invoke(); - } - - public Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration) - { - return addItemFactory.Invoke(); + return addItemFactory(new MockCacheEntry(key)); } public Task GetAsync(string key) @@ -64,53 +37,8 @@ public Task GetAsync(string key) return Task.FromResult(default(T)); } - - public void Add(string key, T item, TimeSpan slidingExpiration) - { - } - public void Add(string key, T item, MemoryCacheEntryOptions policy) { } - - public T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpiration) - { - return addItemFactory.Invoke(); - } - - public T GetOrAdd(string key, Func addItemFactory, MemoryCacheEntryOptions policy) - { - return addItemFactory.Invoke(); - } - } - - public class MockCacheProvider : ICacheProvider - { - public void Set(string key, object item, MemoryCacheEntryOptions policy) - { - } - - public object Get(string key) - { - return null; - } - - public object GetOrCreate(string key, Func func) - { - return func(null); - } - - public void Remove(string key) - { - } - - public Task GetOrCreateAsync(string key, Func> func) - { - return func(null); - } - - public void Dispose() - { - } } } \ No newline at end of file diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 19bd663..290379e 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -11,9 +11,17 @@ `Func DefaultCacheProvider { get; }` By default we use a shared in-memory cache but each instance can have it's underlying cache provider overridden from it's constructor. -- Make methods on CachingService virtual +- Make methods on CachingService virtual/protected to enable - Add LazyCache.AspNetCore for dependency injection registration - ServiceCollection.AddLazyCache(); - Update sample to use aspnet core and LazyCache.AspNetCore +- New IAppCache.DefaultCachePolicy to replace CachingService.DefaultCacheDuration +- Moved most CachingService method overloads to extension methods on IAppCache in AppCacheExtensions. API should be backwards compatible but as now extension methods this is technically an API breaking changing. +- Added new methods on IAppCache to allow you to specify cache expiry options on executution of the item factory + + `GetOrAdd(string key, Func addItemFactory)` + + `Task GetOrAddAsync(string key, Func> addItemFactory)` + ## Version 0.7.1 - Fix async/sync interopability bug, see https://github.com/alastairtree/LazyCache/issues/12 From 6917aca95ff3e880f1aaba006f6dd5d0c9f65f8e Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 4 Mar 2018 15:45:55 +0000 Subject: [PATCH 29/39] docs: README.md --- README.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9675ca3..c9c482e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Lazy cache is a simple in-memory caching service. It has a developer friendly generics based API, and provides a thread safe cache implementation that guarantees to only execute your cachable delegates once (it's lazy!). Under -the hood it leverages ObjectCache and Lazy to provide performance and +the hood it leverages Microsoft.Extensions.Caching and Lazy to provide performance and reliability in heavy load scenarios. ## Download ## @@ -21,7 +21,7 @@ PM> Install-Package LazyCache ```csharp // Create our cache service using the defaults (Dependency injection ready). -// Uses MemoryCache.Default under the hood so cache is shared out of the box +// By default it uses a single shared cache under the hood so cache is shared out of the box (but you can configure this) IAppCache cache = new CachingService(); // Declare (but don't execute) a func/delegate whose result we want to cache @@ -36,9 +36,21 @@ As you can see the magic happens in the `GetOrAdd()` method which gives the cons It means you avoid the usual "Check the cache - execute the factory function - add results to the cache" pattern, saves you writing the double locking cache pattern and means you can be a lazy developer! -## Use case ## +## What should I use it for? + +LazyCache suits the caching of database calls, complex object graph building routines and web service calls that should be cached for performance. +Allows items to be cached for long or short periods, but defaults to 20 mins. + +## .Net framework and dotnet core support? + +The latest version targets netstandard 2.0. See [.net standard implementation support](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support) + +For dotnet core 2, .net framwork net461 or above, netstandard 2+, use LazyCache 2 or above. + +For .net framework without netstandard 2 support such as net45 net451 net46 use LazyCache 0.7 - 1.x + +For .net framework 4.0 use LazyCache 0.6 -Suits the caching of database calls, complex object graph building routines and web service calls that should be cached for performance. Allows items to be cached for long or short periods, but defaults to 20 mins. ## Features ## @@ -48,10 +60,8 @@ Suits the caching of database calls, complex object graph building routines and - Thread safe, concurrency ready - Async compatible - lazy single evaluation of async delegates using `GetOrAddAsync()` - Interface based API and built in `MockCache` to support test driven development and dependency injection -- Leverages ObjectCache under the hood and can be extended with your own implementation of ObjectCache -- The main class `CachingSevice` is a single class and so could be easily embedded in your application or library +- Leverages a provider model on top of IMemoryCache under the hood and can be extended with your own implementation - Good test coverage -- net45 upwards. (for .net4 use Lazycache 0.6) ## Documentation @@ -60,6 +70,6 @@ Suits the caching of database calls, complex object graph building routines and ## Sample Application -See `/Samples/ApiAsyncCachingSample` for an example of how to use LazyCache to cache the results of an Entity framework async query in +See [/Samples/CacheDatabaseQueriesApiSample](https://github.com/alastairtree/LazyCache/tree/master/CacheDatabaseQueriesApiSample) for an example of how to use LazyCache to cache the results of an Entity framework query in a web api controller. Watch how the cache saves trips to the database and results are returned to the client far quicker from the in-memory cache From 5bba5c5dddced74a743eabbc8aca21d164c3cbf4 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 4 Mar 2018 15:48:31 +0000 Subject: [PATCH 30/39] docs: quickstart --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c9c482e..10c39a4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,11 @@ LazyCache is available using [nuget](https://www.nuget.org/packages/LazyCache/). PM> Install-Package LazyCache ``` -## Sample code ## +## Quick start + +See the [quick start wiki](https://github.com/alastairtree/LazyCache/wiki/Quickstart) + +## Sample code ```csharp // Create our cache service using the defaults (Dependency injection ready). @@ -72,4 +76,4 @@ For .net framework 4.0 use LazyCache 0.6 See [/Samples/CacheDatabaseQueriesApiSample](https://github.com/alastairtree/LazyCache/tree/master/CacheDatabaseQueriesApiSample) for an example of how to use LazyCache to cache the results of an Entity framework query in a web api controller. Watch how the cache saves trips to the database and results are returned to the client far quicker from the -in-memory cache +in-memory cache \ No newline at end of file From 0a46273992c2e6e5b0f56cf4d261ec7c73ea49f9 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 13 May 2018 23:10:19 +0100 Subject: [PATCH 31/39] task: some simple tidying of text --- .gitattributes | 63 ++++++++++++++++++++++++++++++++++++++ LazyCache/LazyCache.csproj | 6 ++-- README.md | 4 +-- appveyor.yml | 4 +-- 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/LazyCache/LazyCache.csproj b/LazyCache/LazyCache.csproj index 9ce628c..9820408 100644 --- a/LazyCache/LazyCache.csproj +++ b/LazyCache/LazyCache.csproj @@ -1,4 +1,4 @@ - + @@ -12,13 +12,13 @@ $(LazyCacheVersion).$(AppVeyorBuildNumber) https://github.com/alastairtree https://github.com/alastairtree - Lazy cache is a simple, thread safe in-memory caching service + Lazy cache is a simple, thread safe, in-memory caching library that makes it easy to add high performance caching to your dotnet app. https://github.com/alastairtree/LazyCache Copyright 2014 - 2018 Alastair Crabtree https://github.com/alastairtree/LazyCache/blob/master/LICENSE https://raw.githubusercontent.com/alastairtree/LazyCache/master/artwork/logo-128.png https://github.com/alastairtree/LazyCache - Caching Performance Speed In-memory ObjectCache Generics ServiceCacheing Lazy Cache Lazy-Load MemoryCache CachingService AppCache ApplicationCache Memcached + Caching Performance Speed In-memory IMemoryCache Generics ServiceCacheing Lazy Cache Lazy-Load MemoryCache CachingService AppCache ApplicationCache Memcached See https://raw.githubusercontent.com/alastairtree/LazyCache/master/ReleaseNotes.md diff --git a/README.md b/README.md index 6968763..2e5284d 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,11 @@ For .net framework 4.0 use LazyCache 0.6 ## Documentation -* [the wiki](https://github.com/alastairtree/LazyCache/wiki) +* [The wiki](https://github.com/alastairtree/LazyCache/wiki) * [Adding caching to a .net application and make it faster](https://alastaircrabtree.com/the-easy-way-to-add-caching-to-net-application-and-make-it-faster-is-called-lazycache/) ## Sample Application -See [/Samples/CacheDatabaseQueriesApiSample](https://github.com/alastairtree/LazyCache/tree/master/CacheDatabaseQueriesApiSample) for an example of how to use LazyCache to cache the results of an Entity framework query in +See [CacheDatabaseQueriesApiSample](/CacheDatabaseQueriesApiSample) for an example of how to use LazyCache to cache the results of an Entity framework query in a web api controller. Watch how the cache saves trips to the database and results are returned to the client far quicker from the in-memory cache \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 6580d87..3880b7d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,9 +3,9 @@ image: Visual Studio 2017 configuration: Release environment: LazyCacheVersion: 2.0.0 - LazyCacheVersionSuffix: -beta01 + LazyCacheVersionSuffix: -beta03 LazyCacheAspNetCoreVersion: 2.0.0 - LazyCacheAspNetCoreVersionSuffix: -beta01 + LazyCacheAspNetCoreVersionSuffix: -beta03 build_script: - ps: '& .\build.ps1' deploy: off \ No newline at end of file From e6d043d73b6a83ce733a6455eddbd2bc1308905e Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sun, 13 May 2018 23:11:13 +0100 Subject: [PATCH 32/39] feat: Obsolete default constructors that create IDisposables - in preference for dependency injection instead --- .../CachingServiceMemoryCacheProviderTests.cs | 2 +- LazyCache/CacheDefaults.cs | 1 + LazyCache/CachingService.cs | 92 ++++++++++--------- LazyCache/Providers/MemoryCacheProvider.cs | 4 - 4 files changed, 51 insertions(+), 48 deletions(-) diff --git a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs index 8293fab..85e5e8b 100644 --- a/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs +++ b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs @@ -21,7 +21,7 @@ public void BeforeEachTest() private static CachingService BuildCache() { - return new CachingService(new MemoryCacheProvider()); + return new CachingService(new MemoryCacheProvider(new MemoryCache(new MemoryCacheOptions()))); } private IAppCache sut; diff --git a/LazyCache/CacheDefaults.cs b/LazyCache/CacheDefaults.cs index d76096f..f30b90a 100644 --- a/LazyCache/CacheDefaults.cs +++ b/LazyCache/CacheDefaults.cs @@ -3,6 +3,7 @@ namespace LazyCache { + // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global public class CacheDefaults { public virtual int DefaultCacheDurationSeconds { get; set; } = 60 * 20; diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index 76487bc..f2d51ed 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -7,12 +7,17 @@ namespace LazyCache { + // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global public class CachingService : IAppCache { private readonly Lazy cacheProvider; private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); + [Obsolete("LazyCache is designed for dependency injection, this constructor exists for " + + "backwards compatibility and will be removed at a later date. Consider depending " + + "on IAppCache rather than constructing manually. You may need to dispose of " + + "DefaultCacheProvider.Value if this is used.")] public CachingService() : this(DefaultCacheProvider) { } @@ -34,7 +39,11 @@ public CachingService(ICacheProvider cache) : this(() => cache) } public static Lazy DefaultCacheProvider { get; set; } - = new Lazy(() => new MemoryCacheProvider()); + = new Lazy(() => + new MemoryCacheProvider( + new MemoryCache( + new MemoryCacheOptions()) + )); /// /// Seconds to cache objects for by default @@ -66,7 +75,7 @@ public virtual T Get(string key) var item = CacheProvider.Get(key); - return UnwrapLazy(item); + return GetValueFromLazy(item); } public virtual Task GetAsync(string key) @@ -75,7 +84,7 @@ public virtual Task GetAsync(string key) var item = CacheProvider.Get(key); - return UnwrapAsyncLazys(item); + return GetValueFromAsyncLazy(item); } public virtual T GetOrAdd(string key, Func addItemFactory) @@ -102,7 +111,7 @@ public virtual T GetOrAdd(string key, Func addItemFactory) try { - return UnwrapLazy(cacheItem); + return GetValueFromLazy(cacheItem); } catch //addItemFactory errored so do not cache the exception { @@ -123,23 +132,16 @@ public virtual async Task GetOrAddAsync(string key, Func - // await new AsyncLazy(async () => { - // entry.SetOptions(policy); - // return await addItemFactory.Invoke(); - // } - //).Value); + object cacheItem; - //var cacheItem = CacheProvider.GetOrCreateAsync(key, async entry => - //{ - // entry.SetOptions(policy); - // return new AsyncLazy(async () => await addItemFactory.Invoke()); - //}); + // Ensure only one thread can place an item into the cache provider at a time. + // We are not evaluating the addItemFactory inside here - that happens outside the lock, + // below, and guarded using the async lazy. Here we just ensure only one thread can place + // the AsyncLazy into the cache at one time - object cacheItem; await locker.WaitAsync() - .ConfigureAwait(false); //TODO: do we really need to lock - or can we lock just the key? + .ConfigureAwait( + false); //TODO: do we really need to lock everything here - faster if we could lock on just the key? try { cacheItem = CacheProvider.GetOrCreate(key, entry => @@ -158,7 +160,7 @@ await locker.WaitAsync() try { - var result = UnwrapAsyncLazys(cacheItem); + var result = GetValueFromAsyncLazy(cacheItem); if (result.IsCanceled || result.IsFaulted) CacheProvider.Remove(key); @@ -172,36 +174,40 @@ await locker.WaitAsync() } } - protected virtual T UnwrapLazy(object item) + protected virtual T GetValueFromLazy(object item) { - if (item is Lazy lazy) - return lazy.Value; - - if (item is T variable) - return variable; - - if (item is AsyncLazy asyncLazy) - return asyncLazy.Value.ConfigureAwait(false).GetAwaiter().GetResult(); - - if (item is Task task) - return task.Result; + switch (item) + { + case Lazy lazy: + return lazy.Value; + case T variable: + return variable; + case AsyncLazy asyncLazy: + // this is async to sync - and should not really happen as long as GetOrAddAsync is used for an async + // value. Only happens when you cache something async and then try and grab it again later using + // the non async methods. + return asyncLazy.Value.ConfigureAwait(false).GetAwaiter().GetResult(); + case Task task: + return task.Result; + } return default(T); } - protected virtual Task UnwrapAsyncLazys(object item) + protected virtual Task GetValueFromAsyncLazy(object item) { - if (item is AsyncLazy asyncLazy) - return asyncLazy.Value; - - if (item is Task task) - return task; - - if (item is Lazy lazy) - return Task.FromResult(lazy.Value); - - if (item is T variable) - return Task.FromResult(variable); + switch (item) + { + case AsyncLazy asyncLazy: + return asyncLazy.Value; + case Task task: + return task; + // this is sync to async and only happens if you cache something sync and then get it later async + case Lazy lazy: + return Task.FromResult(lazy.Value); + case T variable: + return Task.FromResult(variable); + } return Task.FromResult(default(T)); } diff --git a/LazyCache/Providers/MemoryCacheProvider.cs b/LazyCache/Providers/MemoryCacheProvider.cs index a2e9396..9bdfdc9 100644 --- a/LazyCache/Providers/MemoryCacheProvider.cs +++ b/LazyCache/Providers/MemoryCacheProvider.cs @@ -8,10 +8,6 @@ public class MemoryCacheProvider : ICacheProvider { internal readonly IMemoryCache cache; - public MemoryCacheProvider() : this(new MemoryCache(new MemoryCacheOptions())) - { - } - public MemoryCacheProvider(IMemoryCache cache) { this.cache = cache; From ecb0d74f94f504ef9aa9e139842837e912dacbe2 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Sat, 23 Feb 2019 13:07:09 +0000 Subject: [PATCH 33/39] feat: add a net461 sample --- Console.Net461/App.config | 6 ++ Console.Net461/Console.Net461.csproj | 92 +++++++++++++++++++++++ Console.Net461/Program.cs | 21 ++++++ Console.Net461/Properties/AssemblyInfo.cs | 36 +++++++++ Console.Net461/packages.config | 13 ++++ LazyCache.sln | 9 ++- 6 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 Console.Net461/App.config create mode 100644 Console.Net461/Console.Net461.csproj create mode 100644 Console.Net461/Program.cs create mode 100644 Console.Net461/Properties/AssemblyInfo.cs create mode 100644 Console.Net461/packages.config diff --git a/Console.Net461/App.config b/Console.Net461/App.config new file mode 100644 index 0000000..731f6de --- /dev/null +++ b/Console.Net461/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Console.Net461/Console.Net461.csproj b/Console.Net461/Console.Net461.csproj new file mode 100644 index 0000000..cc92b28 --- /dev/null +++ b/Console.Net461/Console.Net461.csproj @@ -0,0 +1,92 @@ + + + + + Debug + AnyCPU + {32142F20-DFCE-4DF0-A263-093111E5A3FA} + Exe + Console.Net461 + Console.Net461 + v4.6.1 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Microsoft.Extensions.Caching.Abstractions.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Caching.Memory.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Memory.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.2.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Options.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Options.dll + + + ..\packages\Microsoft.Extensions.Primitives.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll + + + + ..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll + + + ..\packages\System.ComponentModel.Annotations.4.5.0\lib\net461\System.ComponentModel.Annotations.dll + + + + + ..\packages\System.Memory.4.5.1\lib\netstandard2.0\System.Memory.dll + + + + ..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.1\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + + + + + + + + + + + + + + + + + {E6A1EF20-94AD-4A1C-9A89-3B2FA8AD8EC7} + LazyCache + + + + \ No newline at end of file diff --git a/Console.Net461/Program.cs b/Console.Net461/Program.cs new file mode 100644 index 0000000..6c1c649 --- /dev/null +++ b/Console.Net461/Program.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using LazyCache; + +namespace Console.Net461 +{ + class Program + { + static void Main(string[] args) + { + IAppCache cache = new CachingService(CachingService.DefaultCacheProvider); + + var item = cache.GetOrAdd("Program.Main.Person", () => Tuple.Create("Joe Blogs", DateTime.UtcNow)); + + System.Console.WriteLine(item.Item1); + } + } +} diff --git a/Console.Net461/Properties/AssemblyInfo.cs b/Console.Net461/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..593ecab --- /dev/null +++ b/Console.Net461/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Console.Net461")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Console.Net461")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("32142f20-dfce-4df0-a263-093111e5a3fa")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Console.Net461/packages.config b/Console.Net461/packages.config new file mode 100644 index 0000000..7fb8e3c --- /dev/null +++ b/Console.Net461/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/LazyCache.sln b/LazyCache.sln index 086b371..00183dd 100644 --- a/LazyCache.sln +++ b/LazyCache.sln @@ -19,7 +19,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{335B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheDatabaseQueriesApiSample", "CacheDatabaseQueriesApiSample\CacheDatabaseQueriesApiSample.csproj", "{5D6A88DD-230C-4057-B8EB-A987FF4F29DB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyCache.AspNetCore", "LazyCache.AspNetCore\LazyCache.AspNetCore.csproj", "{A7B07002-29F5-4463-8CA7-097C337337A1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LazyCache.AspNetCore", "LazyCache.AspNetCore\LazyCache.AspNetCore.csproj", "{A7B07002-29F5-4463-8CA7-097C337337A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Console.Net461", "Console.Net461\Console.Net461.csproj", "{32142F20-DFCE-4DF0-A263-093111E5A3FA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -43,12 +45,17 @@ Global {A7B07002-29F5-4463-8CA7-097C337337A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {A7B07002-29F5-4463-8CA7-097C337337A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {A7B07002-29F5-4463-8CA7-097C337337A1}.Release|Any CPU.Build.0 = Release|Any CPU + {32142F20-DFCE-4DF0-A263-093111E5A3FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32142F20-DFCE-4DF0-A263-093111E5A3FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32142F20-DFCE-4DF0-A263-093111E5A3FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32142F20-DFCE-4DF0-A263-093111E5A3FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {5D6A88DD-230C-4057-B8EB-A987FF4F29DB} = {335BA426-C839-4996-8476-F3EE4056C40E} + {32142F20-DFCE-4DF0-A263-093111E5A3FA} = {335BA426-C839-4996-8476-F3EE4056C40E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5040E431-0FAA-4DC7-A678-D218CD57D542} From f719c6bd42f33639d7573b57857a4954991f8602 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Mon, 25 Feb 2019 13:39:47 +0000 Subject: [PATCH 34/39] chore: fix license to use new nuget license warnings --- LazyCache.AspNetCore/LazyCache.AspNetCore.csproj | 5 +++-- LazyCache/LazyCache.csproj | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj index a195143..951facd 100644 --- a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj +++ b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj @@ -1,4 +1,4 @@ - + @@ -15,13 +15,14 @@ https://github.com/alastairtree ServiceCollection regististrations fopr LazyCache to initialise the depndency injection Copyright 2014 - 2018 Alastair Crabtree - https://github.com/alastairtree/LazyCache/blob/master/LICENSE https://github.com/alastairtree/LazyCache https://raw.githubusercontent.com/alastairtree/LazyCache/master/artwork/logo-128.png https://github.com/alastairtree/LazyCache LazyCache DependecyInjection ServiceCollection SingleTon Transient + MIT + diff --git a/LazyCache/LazyCache.csproj b/LazyCache/LazyCache.csproj index 9820408..432e3b8 100644 --- a/LazyCache/LazyCache.csproj +++ b/LazyCache/LazyCache.csproj @@ -15,11 +15,11 @@ Lazy cache is a simple, thread safe, in-memory caching library that makes it easy to add high performance caching to your dotnet app. https://github.com/alastairtree/LazyCache Copyright 2014 - 2018 Alastair Crabtree - https://github.com/alastairtree/LazyCache/blob/master/LICENSE https://raw.githubusercontent.com/alastairtree/LazyCache/master/artwork/logo-128.png https://github.com/alastairtree/LazyCache Caching Performance Speed In-memory IMemoryCache Generics ServiceCacheing Lazy Cache Lazy-Load MemoryCache CachingService AppCache ApplicationCache Memcached See https://raw.githubusercontent.com/alastairtree/LazyCache/master/ReleaseNotes.md + MIT From c6584c222f77757be129b6e615a62955f9e4086d Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Mon, 25 Feb 2019 14:00:36 +0000 Subject: [PATCH 35/39] feat: restore console app before build using nuget dotnet cli does not work nicely for 461 projects --- build.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/build.ps1 b/build.ps1 index 6e01ea6..d0f8839 100644 --- a/build.ps1 +++ b/build.ps1 @@ -32,6 +32,7 @@ Try { # Get dependencies from nuget and compile Exec { dotnet restore } + Exec { nuget restore Console.Net461 -SolutionDirectory . } Exec { dotnet build --configuration $config --no-restore } # Find each test project and run tests. upload results to AppVeyor From 14cc9d0f270a1c800cbd95703396cd7b7e9a84e0 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Mon, 25 Feb 2019 14:24:50 +0000 Subject: [PATCH 36/39] task: update dependencies --- .../CacheDatabaseQueriesApiSample.csproj | 6 +++--- Console.Net461/App.config | 18 +++++++++++++++++- Console.Net461/Console.Net461.csproj | 14 +++++++------- Console.Net461/packages.config | 8 ++++---- .../LazyCache.AspNetCore.csproj | 6 +++--- LazyCache.UnitTests/LazyCache.UnitTests.csproj | 10 +++++----- LazyCache/LazyCache.csproj | 4 ++-- 7 files changed, 41 insertions(+), 25 deletions(-) diff --git a/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj b/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj index 8e82000..0156a85 100644 --- a/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj +++ b/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj @@ -1,12 +1,12 @@ - netcoreapp2.0 + netcoreapp2.2 - - + + diff --git a/Console.Net461/App.config b/Console.Net461/App.config index 731f6de..c244acc 100644 --- a/Console.Net461/App.config +++ b/Console.Net461/App.config @@ -1,6 +1,22 @@ - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Console.Net461/Console.Net461.csproj b/Console.Net461/Console.Net461.csproj index cc92b28..c16cd8a 100644 --- a/Console.Net461/Console.Net461.csproj +++ b/Console.Net461/Console.Net461.csproj @@ -49,8 +49,8 @@ ..\packages\Microsoft.Extensions.Primitives.2.2.0\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll - - ..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll + + ..\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll ..\packages\System.ComponentModel.Annotations.4.5.0\lib\net461\System.ComponentModel.Annotations.dll @@ -58,14 +58,14 @@ - ..\packages\System.Memory.4.5.1\lib\netstandard2.0\System.Memory.dll + ..\packages\System.Memory.4.5.2\lib\netstandard2.0\System.Memory.dll - - ..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.1\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll diff --git a/Console.Net461/packages.config b/Console.Net461/packages.config index 7fb8e3c..d68252b 100644 --- a/Console.Net461/packages.config +++ b/Console.Net461/packages.config @@ -5,9 +5,9 @@ - + - - - + + + \ No newline at end of file diff --git a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj index 951facd..2939774 100644 --- a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj +++ b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj @@ -24,9 +24,9 @@ - - - + + + diff --git a/LazyCache.UnitTests/LazyCache.UnitTests.csproj b/LazyCache.UnitTests/LazyCache.UnitTests.csproj index b5e5ad0..84c55c2 100644 --- a/LazyCache.UnitTests/LazyCache.UnitTests.csproj +++ b/LazyCache.UnitTests/LazyCache.UnitTests.csproj @@ -1,16 +1,16 @@ - netcoreapp2.0 + netcoreapp2.2 false - - - - + + + + diff --git a/LazyCache/LazyCache.csproj b/LazyCache/LazyCache.csproj index 432e3b8..e412b30 100644 --- a/LazyCache/LazyCache.csproj +++ b/LazyCache/LazyCache.csproj @@ -23,8 +23,8 @@ - - + + From 2f7bb4a396bacf0e4332c0c1063fca6dbec21065 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Mon, 25 Feb 2019 14:28:34 +0000 Subject: [PATCH 37/39] bug: use newer Microsoft.AspNetCore.App package - update reference in sample app --- .../CacheDatabaseQueriesApiSample.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj b/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj index 0156a85..18be97f 100644 --- a/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj +++ b/CacheDatabaseQueriesApiSample/CacheDatabaseQueriesApiSample.csproj @@ -1,11 +1,11 @@ - + netcoreapp2.2 - + From eeb408a300939ef4781ff270462e6be162c2ffc8 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Wed, 20 Mar 2019 21:56:35 +0000 Subject: [PATCH 38/39] task: undo deprecating default constructor Fixes #44 --- LazyCache/CachingService.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs index f2d51ed..1bd5305 100644 --- a/LazyCache/CachingService.cs +++ b/LazyCache/CachingService.cs @@ -14,10 +14,6 @@ public class CachingService : IAppCache private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); - [Obsolete("LazyCache is designed for dependency injection, this constructor exists for " + - "backwards compatibility and will be removed at a later date. Consider depending " + - "on IAppCache rather than constructing manually. You may need to dispose of " + - "DefaultCacheProvider.Value if this is used.")] public CachingService() : this(DefaultCacheProvider) { } From 2c3b2ba14662a24cc4663c0b012a8e21864bf083 Mon Sep 17 00:00:00 2001 From: Alastair Crabtree Date: Wed, 20 Mar 2019 21:57:55 +0000 Subject: [PATCH 39/39] task: release version 2.0.0 --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 3880b7d..97ec692 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,9 +3,9 @@ image: Visual Studio 2017 configuration: Release environment: LazyCacheVersion: 2.0.0 - LazyCacheVersionSuffix: -beta03 + LazyCacheVersionSuffix: LazyCacheAspNetCoreVersion: 2.0.0 - LazyCacheAspNetCoreVersionSuffix: -beta03 + LazyCacheAspNetCoreVersionSuffix: build_script: - ps: '& .\build.ps1' deploy: off \ No newline at end of file