From 079c4fbbb31bca1a4f07a02070a5b0b551e09166 Mon Sep 17 00:00:00 2001 From: adobarganes1 Date: Fri, 22 Mar 2019 03:29:49 -0400 Subject: [PATCH 1/4] work in progress, having issues with serialization --- .gitignore | 1 + .../DistributedCachingServiceProviderTests.cs | 704 ++++++++++++++++++ LazyCache/DistributedAppCacheExtensions.cs | 94 +++ LazyCache/DistributedCacheDefaults.cs | 19 + LazyCache/DistributedCacheEntry.cs | 38 + LazyCache/DistributedCachingService.cs | 196 +++++ LazyCache/IDistributedAppCache.cs | 28 + LazyCache/IDistributedCacheProvider.cs | 13 + LazyCache/LazyCache.csproj | 3 + .../Providers/DistributedCacheProvider.cs | 120 +++ 10 files changed, 1216 insertions(+) create mode 100644 LazyCache.UnitTests/DistributedCachingServiceProviderTests.cs create mode 100644 LazyCache/DistributedAppCacheExtensions.cs create mode 100644 LazyCache/DistributedCacheDefaults.cs create mode 100644 LazyCache/DistributedCacheEntry.cs create mode 100644 LazyCache/DistributedCachingService.cs create mode 100644 LazyCache/IDistributedAppCache.cs create mode 100644 LazyCache/IDistributedCacheProvider.cs create mode 100644 LazyCache/Providers/DistributedCacheProvider.cs diff --git a/.gitignore b/.gitignore index bd4a32b..c676e42 100644 --- a/.gitignore +++ b/.gitignore @@ -190,3 +190,4 @@ $RECYCLE.BIN/ packages/ .vs/ +/.vscode diff --git a/LazyCache.UnitTests/DistributedCachingServiceProviderTests.cs b/LazyCache.UnitTests/DistributedCachingServiceProviderTests.cs new file mode 100644 index 0000000..8f65294 --- /dev/null +++ b/LazyCache.UnitTests/DistributedCachingServiceProviderTests.cs @@ -0,0 +1,704 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using LazyCache.Providers; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using NUnit.Framework; + +namespace LazyCache.UnitTests +{ + [TestFixture] + public class DistributedCachingProviderTests + { + [SetUp] + public void BeforeEachTest() + { + sut = BuildCache(); + testObject = new ComplexTestObject(); + } + + private static DistributedCachingService BuildCache() + { + var options = new MemoryDistributedCacheOptions(); + IDistributedCache memoryDistributedCache = new MemoryDistributedCache(Options.Create(options)); + IDistributedCacheProvider distributedCacheProvider = new DistributedCacheProvider(memoryDistributedCache); + return new DistributedCachingService(distributedCacheProvider); + } + + private IDistributedAppCache sut; + + private readonly DistributedCacheEntryOptions oneHourNonRemoveableDistributedCacheEntryOptions = + new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) + }; + + private ComplexTestObject testObject = new ComplexTestObject(); + + private class ComplexTestObject + { + public readonly IList SomeItems = new List { 1, 2, 3, "testing123" }; + public string SomeMessage = "testing123"; + } + + private const string TestKey = "testKey"; + + + [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); + //Not holding the references! + //Assert.AreEqual(expected, actual); + testObject.SomeItems.Should().Contain("Another"); + testObject.SomeMessage.Should().Be("changed-it-up"); + } + + [Test] + public void AddComplexObjectThenGetReturnsCachedObject() + { + sut.Add(TestKey, testObject); + //Not unboxing + var actual = sut.Get(TestKey); + var expected = testObject; + Assert.NotNull(actual); + //Differenct reference + //Assert.AreEqual(expected, actual); + } + + [Test] + public void AddEmptyKeyThrowsException() + { + Action act = () => sut.Add("", new object()); + act.Should().Throw(); + } + + [Test] + public void AddEmptyKeyThrowsExceptionWithExpiration() + { + Action act = () => sut.Add("", new object(), DateTimeOffset.Now.AddHours(1)); + act.Should().Throw(); + } + + [Test] + public void AddEmptyKeyThrowsExceptionWithPolicy() + { + Action act = () => sut.Add("", new object(), new DistributedCacheEntryOptions()); + act.Should().Throw(); + } + + [Test] + public void AddEmptyKeyThrowsExceptionWithSliding() + { + Action act = () => sut.Add("", new object(), new TimeSpan(1000)); + act.Should().Throw(); + } + + [Test] + public void AddNullKeyThrowsException() + { + Action act = () => sut.Add(null, new object()); + act.Should().Throw(); + } + + [Test] + public void AddNullKeyThrowsExceptionWithExpiration() + { + Action act = () => sut.Add(null, new object(), DateTimeOffset.Now.AddHours(1)); + act.Should().Throw(); + } + + [Test] + public void AddNullKeyThrowsExceptionWithPolicy() + { + Action act = () => sut.Add(null, new object(), new DistributedCacheEntryOptions()); + act.Should().Throw(); + } + + [Test] + public void AddNullKeyThrowsExceptionWithSliding() + { + Action act = () => sut.Add(null, new object(), new TimeSpan(1000)); + act.Should().Throw(); + } + + [Test] + public void AddNullThrowsException() + { + Action act = () => sut.Add(TestKey, null); + act.Should().Throw(); + } + + [Test] + public void AddThenGetReturnsCachedObject() + { + sut.Add(TestKey, "testObject"); + Assert.AreEqual("testObject", sut.Get(TestKey)); + } + + [Test] + public void AddWithOffsetReturnsCachedItem() + { + sut.Add(TestKey, "testObject", DateTimeOffset.Now.AddSeconds(1)); + Assert.AreEqual("testObject", sut.Get(TestKey)); + } + + [Test] + public void AddWithOffsetThatExpiresReturnsNull() + { + sut.Add(TestKey, "testObject", DateTimeOffset.Now.AddSeconds(1)); + Thread.Sleep(1500); + Assert.IsNull(sut.Get(TestKey)); + } + + [Test] + public void AddWithPolicyReturnsCachedItem() + { + sut.Add(TestKey, "testObject", new DistributedCacheEntryOptions()); + Assert.AreEqual("testObject", sut.Get(TestKey)); + } + + [Test] + public void AddWithSlidingReturnsCachedItem() + { + sut.Add(TestKey, "testObject", new TimeSpan(5000)); + Assert.AreEqual("testObject", sut.Get(TestKey)); + } + + [Test] + public void AddWithSlidingThatExpiresReturnsNull() + { + sut.Add(TestKey, "testObject", new TimeSpan(750)); + Thread.Sleep(1500); + Assert.IsNull(sut.Get(TestKey)); + } + + [Test] + public void CacheProviderIsNotNull() + { + sut.DistributedCacheProvider.Should().NotBeNull(); + } + + [Test] + public void DefaultContructorThenGetOrAddFromSecondCachingServiceHasSharedUnderlyingCache() + { + var options = new MemoryDistributedCacheOptions(); + IDistributedCache memoryDistributedCache = new MemoryDistributedCache(Options.Create(options)); + IDistributedCacheProvider distributedCacheProvider = new DistributedCacheProvider(memoryDistributedCache); + var cacheOne = new DistributedCachingService(distributedCacheProvider); + var cacheTwo = new DistributedCachingService(distributedCacheProvider); + + 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() + { + DateTime? cached = new DateTime(); + sut.Add(TestKey, cached); + Assert.AreEqual(cached.Value, sut.Get(TestKey)); + } + + [Test] + public void GetEmptyKeyThrowsException() + { + Action act = () => sut.Get(""); + act.Should().Throw(); + } + + [Test] + public void GetFromCacheTwiceAtSameTimeOnlyAddsOnce() + { + var times = 0; + + var t1 = Task.Factory.StartNew(() => + { + sut.GetOrAdd(TestKey, () => + { + Interlocked.Increment(ref times); + return new DateTime(2001, 01, 01); + }); + }); + + var t2 = Task.Factory.StartNew(() => + { + sut.GetOrAdd(TestKey, () => + { + Interlocked.Increment(ref times); + return new DateTime(2001, 01, 01); + }); + }); + + Task.WaitAll(t1, t2); + + Assert.AreEqual(1, times); + } + + [Test] + public void GetNullKeyThrowsException() + { + Action act = () => sut.Get(null); + act.Should().Throw(); + } + + [Test] + public void GetOrAddAndThenGetObjectReturnsCorrectType() + { + sut.GetOrAdd(TestKey, () => testObject); + var actual = sut.Get(TestKey); + Assert.IsNotNull(actual); + } + + [Test] + public void GetOrAddAndThenGetValueObjectReturnsCorrectType() + { + sut.GetOrAdd(TestKey, () => 123); + var actual = sut.Get(TestKey); + Assert.AreEqual(123, actual); + } + + [Test] + public void GetOrAddAndThenGetWrongtypeObjectReturnsNull() + { + sut.GetOrAdd(TestKey, () => testObject); + var actual = sut.Get(TestKey); + Assert.IsNull(actual); + } + + [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); + } + + [Test] + public void GetOrAddAsyncAFailingTaskDoesNotCacheIt() + { + Task FetchAsync() + { + return 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 async Task GetOrAddAsyncAndThenGetAsyncObjectReturnsCorrectType() + { + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(testObject)); + var actual = await sut.GetAsync(TestKey); + Assert.IsNotNull(actual); + Assert.That(actual, Is.EqualTo(testObject)); + } + + [Test] + public async Task GetOrAddAsyncAndThenGetAsyncWrongObjectReturnsNull() + { + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(testObject)); + var actual = await sut.GetAsync(TestKey); + Assert.IsNull(actual); + } + + [Test] + public async Task GetOrAddAsyncFollowinGetOrAddReturnsTheFirstObjectAndIgnoresTheSecondTask() + { + ComplexTestObject FetchSync() + { + return testObject; + } + + 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)); + + Assert.IsNotNull(actualAsync); + Assert.That(actualAsync, Is.EqualTo(testObject)); + + Assert.AreEqual(actualAsync, actualSync); + } + + [Test] + public async Task GetOrAddAsyncTaskAndThenGetTaskOfAnotherTypeReturnsNull() + { + var cachedAsyncResult = testObject; + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(cachedAsyncResult)); + var actual = sut.Get>(TestKey); + Assert.Null(actual); + } + + [Test] + public async Task GetOrAddAsyncTaskAndThenGetTaskOfObjectReturnsCorrectType() + { + var cachedAsyncResult = testObject; + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(cachedAsyncResult)); + var actual = sut.Get>(TestKey); + Assert.IsNotNull(actual); + Assert.That(actual.Result, Is.EqualTo(cachedAsyncResult)); + } + + [Test] + public async Task GetOrAddAsyncWillAddOnFirstCall() + { + var times = 0; + + var expected = await sut.GetOrAddAsync(TestKey, () => + { + times++; + return Task.FromResult(new DateTime(2001, 01, 01)); + }); + Assert.AreEqual(2001, expected.Year); + Assert.AreEqual(1, times); + } + + + [Test] + public async Task GetOrAddAsyncWillAddOnFirstCallButReturnCachedOnSecond() + { + var times = 0; + + var expectedFirst = sut.GetOrAdd(TestKey, () => + { + times++; + return new DateTime(2001, 01, 01); + }); + + var expectedSecond = sut.GetOrAdd(TestKey, () => + { + times++; + return new DateTime(2002, 01, 01); + }); + + Assert.AreEqual(2001, expectedFirst.Year); + Assert.AreEqual(2001, expectedSecond.Year); + Assert.AreEqual(1, times); + } + + [Test] + public async Task GetOrAddAsyncWillNotAddIfExistingData() + { + var times = 0; + + var cached = new DateTime(1999, 01, 01); + sut.Add(TestKey, cached); + + var expected = await sut.GetOrAddAsync(TestKey, () => + { + times++; + return Task.FromResult(new DateTime(2001, 01, 01)); + }); + Assert.AreEqual(1999, expected.Year); + Assert.AreEqual(0, times); + } + + [Test] + [MaxTime(1000)] + public void GetOrAddAsyncWithALongTaskReturnsBeforeTaskCompletes() + { + var cachedResult = testObject; + + 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] + public async Task GetOrAddAsyncWithOffsetWillAddAndReturnTaskOfCached() + { + var expectedFirst = await sut.GetOrAddAsync( + TestKey, + () => Task.FromResult(new DateTime(2001, 01, 01)), + DateTimeOffset.Now.AddSeconds(5) + ); + var expectedSecond = await sut.Get>(TestKey); + + Assert.AreEqual(2001, expectedFirst.Year); + Assert.AreEqual(2001, expectedSecond.Year); + } + + [Test] + public async Task GetOrAddAsyncWithPolicyAndThenGetTaskObjectReturnsCorrectType() + { + var item = testObject; + await sut.GetOrAddAsync(TestKey, () => Task.FromResult(item), + oneHourNonRemoveableDistributedCacheEntryOptions); + var actual = await sut.Get>(TestKey); + Assert.That(actual, Is.EqualTo(item)); + } + + + [Test] + public async Task GetOrAddAyncAllowsCachingATask() + { + var cachedResult = testObject; + + Task FetchAsync() + { + return Task.FromResult(cachedResult); + } + + var actualResult = + await sut.GetOrAddAsync(TestKey, FetchAsync, oneHourNonRemoveableDistributedCacheEntryOptions); + + Assert.That(actualResult, Is.EqualTo(cachedResult)); + } + + [Test] + public async Task GetOrAddFollowinGetOrAddAsyncReturnsTheFirstObjectAndUnwrapsTheFirstTask() + { + Task FetchAsync() + { + return Task.FromResult(testObject); + } + + 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)); + + Assert.IsNotNull(actualSync); + Assert.That(actualSync, Is.EqualTo(testObject)); + + Assert.AreEqual(actualAsync, actualSync); + } + + [Test] + public void GetOrAddWillAddOnFirstCall() + { + var times = 0; + + + var expected = sut.GetOrAdd(TestKey, () => + { + times++; + return new DateTime(2001, 01, 01); + }); + Assert.AreEqual(2001, expected.Year); + Assert.AreEqual(1, times); + } + + + [Test] + public void GetOrAddWillAddOnFirstCallButReturnCachedOnSecond() + { + var times = 0; + + var expectedFirst = sut.GetOrAdd(TestKey, () => + { + times++; + return new DateTime(2001, 01, 01); + }); + + var expectedSecond = sut.GetOrAdd(TestKey, () => + { + times++; + return new DateTime(2002, 01, 01); + }); + + Assert.AreEqual(2001, expectedFirst.Year); + Assert.AreEqual(2001, expectedSecond.Year); + Assert.AreEqual(1, times); + } + + [Test] + public void GetOrAddWillNotAddIfExistingData() + { + var times = 0; + + var cached = new DateTime(1999, 01, 01); + sut.Add(TestKey, cached); + + var expected = sut.GetOrAdd(TestKey, () => + { + times++; + return new DateTime(2001, 01, 01); + }); + Assert.AreEqual(1999, expected.Year); + Assert.AreEqual(0, times); + } + + [Test] + public void GetOrAddWithOffsetWillAddAndReturnCached() + { + var expectedFirst = sut.GetOrAdd( + TestKey, + () => new DateTime(2001, 01, 01), + DateTimeOffset.Now.AddSeconds(5) + ); + var expectedSecond = sut.Get(TestKey); + + Assert.AreEqual(2001, expectedFirst.Year); + Assert.AreEqual(2001, expectedSecond.Year); + } + + [Test] + public void GetOrAddWithPolicyAndThenGetObjectReturnsCorrectType() + { + sut.GetOrAdd(TestKey, () => testObject, + oneHourNonRemoveableDistributedCacheEntryOptions); + var actual = sut.Get(TestKey); + Assert.IsNotNull(actual); + } + + [Test] + public void GetOrAddWithPolicyAndThenGetValueObjectReturnsCorrectType() + { + int Fetch() + { + return 123; + } + + sut.GetOrAdd(TestKey, Fetch, oneHourNonRemoveableDistributedCacheEntryOptions); + var actual = sut.Get(TestKey); + Assert.AreEqual(123, actual); + } + + [Test] + public void GetOrAddWithPolicyWillAddOnFirstCallButReturnCachedOnSecond() + { + var times = 0; + + + var expectedFirst = sut.GetOrAdd(TestKey, () => + { + times++; + return new DateTime(2001, 01, 01); + }, oneHourNonRemoveableDistributedCacheEntryOptions); + + var expectedSecond = sut.GetOrAdd(TestKey, () => + { + times++; + return new DateTime(2002, 01, 01); + }, oneHourNonRemoveableDistributedCacheEntryOptions); + + Assert.AreEqual(2001, expectedFirst.Year); + Assert.AreEqual(2001, expectedSecond.Year); + Assert.AreEqual(1, times); + } + + + [Test] + public void GetWithClassTypeParamReturnsType() + { + var cached = new EventArgs(); + sut.Add(TestKey, cached); + Assert.AreEqual(cached, sut.Get(TestKey)); + } + + [Test] + public void GetWithIntRetunsDefaultIfNotCached() + { + Assert.AreEqual(default(int), sut.Get(TestKey)); + } + + [Test] + public void GetWithNullableIntRetunsCachedNonNullableInt() + { + const int expected = 123; + sut.Add(TestKey, expected); + Assert.AreEqual(expected, sut.Get(TestKey)); + } + + [Test] + public void GetWithNullableStructTypeParamReturnsType() + { + var cached = new DateTime(); + sut.Add(TestKey, cached); + Assert.AreEqual(cached, sut.Get(TestKey)); + } + + [Test] + public void GetWithStructTypeParamReturnsType() + { + var cached = new DateTime(2000, 1, 1); + sut.Add(TestKey, cached); + Assert.AreEqual(cached, sut.Get(TestKey)); + } + + [Test] + public void GetWithValueTypeParamReturnsType() + { + const int cached = 3; + sut.Add(TestKey, cached); + Assert.AreEqual(3, sut.Get(TestKey)); + } + + [Test] + public void GetWithWrongClassTypeParamReturnsNull() + { + var cached = new EventArgs(); + sut.Add(TestKey, cached); + Assert.IsNull(sut.Get(TestKey)); + } + + [Test] + public void GetWithWrongStructTypeParamReturnsNull() + { + var cached = new DateTime(); + sut.Add(TestKey, cached); + Assert.AreEqual(new TimeSpan(), sut.Get(TestKey)); + } + + [Test] + public void RemovedItemCannotBeRetrievedFromCache() + { + sut.Add(TestKey, new object()); + Assert.NotNull(sut.Get(TestKey)); + sut.Remove(TestKey); + Assert.Null(sut.Get(TestKey)); + } + } +} \ No newline at end of file diff --git a/LazyCache/DistributedAppCacheExtensions.cs b/LazyCache/DistributedAppCacheExtensions.cs new file mode 100644 index 0000000..e0515a6 --- /dev/null +++ b/LazyCache/DistributedAppCacheExtensions.cs @@ -0,0 +1,94 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; + +namespace LazyCache +{ +public static class DistributedAppCacheExtenions + + { + public static void Add(this IDistributedAppCache 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 IDistributedAppCache cache, string key, T item, DateTimeOffset expires) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + cache.Add(key, item, new DistributedCacheEntryOptions { AbsoluteExpiration = expires }); + } + + public static void Add(this IDistributedAppCache cache, string key, T item, TimeSpan slidingExpiration) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + cache.Add(key, item, new DistributedCacheEntryOptions { SlidingExpiration = slidingExpiration }); + } + + public static T GetOrAdd(this IDistributedAppCache 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 IDistributedAppCache cache, string key, Func addItemFactory, DateTimeOffset expires) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAdd(key, addItemFactory, new DistributedCacheEntryOptions { AbsoluteExpiration = expires }); + } + + public static T GetOrAdd(this IDistributedAppCache cache, string key, Func addItemFactory, TimeSpan slidingExpiration) + { + return cache.GetOrAdd(key, addItemFactory, + new DistributedCacheEntryOptions { SlidingExpiration = slidingExpiration }); + } + + public static T GetOrAdd(this IDistributedAppCache cache, string key, Func addItemFactory, DistributedCacheEntryOptions policy) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAdd(key, entry => + { + entry.SetOptions(policy); + return addItemFactory(); + }); + } + + public static Task GetOrAddAsync(this IDistributedAppCache 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 IDistributedAppCache cache, string key, Func> addItemFactory, DateTimeOffset expires) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAddAsync(key, addItemFactory, new DistributedCacheEntryOptions { AbsoluteExpiration = expires }); + } + + public static Task GetOrAddAsync(this IDistributedAppCache cache, string key, Func> addItemFactory, TimeSpan slidingExpiration) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + return cache.GetOrAddAsync(key, addItemFactory, new DistributedCacheEntryOptions { SlidingExpiration = slidingExpiration }); + } + + public static Task GetOrAddAsync(this IDistributedAppCache cache, string key, Func> addItemFactory, DistributedCacheEntryOptions 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/DistributedCacheDefaults.cs b/LazyCache/DistributedCacheDefaults.cs new file mode 100644 index 0000000..629de0d --- /dev/null +++ b/LazyCache/DistributedCacheDefaults.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.Extensions.Caching.Distributed; + +namespace LazyCache +{ + public class DistributedCacheDefaults + { + public virtual int DefaultCacheDurationSeconds { get; set; } = 60 * 20; + + internal DistributedCacheEntryOptions BuildOptions() + { + return new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTimeOffset.UtcNow.AddSeconds(DefaultCacheDurationSeconds), + SlidingExpiration = TimeSpan.FromSeconds(200) + }; + } + } +} \ No newline at end of file diff --git a/LazyCache/DistributedCacheEntry.cs b/LazyCache/DistributedCacheEntry.cs new file mode 100644 index 0000000..a8d80ee --- /dev/null +++ b/LazyCache/DistributedCacheEntry.cs @@ -0,0 +1,38 @@ +using LazyCache; +using Microsoft.Extensions.Caching.Distributed; + +public sealed class DistributedCacheEntry +{ + public string Key { get; internal set; } + + public object Value { get; internal set; } + + public DistributedCacheEntryOptions DistributedCacheEntryOptions { get; private set; } + + + public void SetOptions(DistributedCacheEntryOptions options) + { + DistributedCacheEntryOptions = options; + } + + public DistributedCacheEntry(string key, object value, DistributedCacheEntryOptions distributedCacheEntryOptions) : this(key, distributedCacheEntryOptions) + { + Value = value; + } + + public DistributedCacheEntry(string key, DistributedCacheEntryOptions distributedCacheEntryOptions) : this(key) + { + DistributedCacheEntryOptions = distributedCacheEntryOptions; + } + + public DistributedCacheEntry(string key) + { + Key = key; + DistributedCacheEntryOptions = new DistributedCacheDefaults().BuildOptions(); + } + + public void SetValue(object value) + { + Value = value; + } +} \ No newline at end of file diff --git a/LazyCache/DistributedCachingService.cs b/LazyCache/DistributedCachingService.cs new file mode 100644 index 0000000..8971ffa --- /dev/null +++ b/LazyCache/DistributedCachingService.cs @@ -0,0 +1,196 @@ +using Microsoft.Extensions.Caching.Distributed; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LazyCache +{ + public class DistributedCachingService : IDistributedAppCache + { + private readonly Lazy cacheProvider; + + private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); + + public DistributedCachingService(Lazy cacheProvider) + { + this.cacheProvider = cacheProvider ?? throw new ArgumentNullException(nameof(cacheProvider)); + } + + public DistributedCachingService(Func cacheProviderFactory) + { + if (cacheProviderFactory == null) throw new ArgumentNullException(nameof(cacheProviderFactory)); + cacheProvider = new Lazy(cacheProviderFactory); + } + + public DistributedCachingService(IDistributedCacheProvider cache) : this(() => cache) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + } + + /// + /// Seconds to cache objects for by default + /// + [Obsolete("DefaultCacheDuration has been replaced with DefaultCacheDurationSeconds")] + public virtual int DefaultCacheDuration + { + get => DefaultCachePolicy.DefaultCacheDurationSeconds; + set => DefaultCachePolicy.DefaultCacheDurationSeconds = value; + } + + public virtual IDistributedCacheProvider DistributedCacheProvider => cacheProvider.Value; + + /// + /// Policy defining how long items should be cached for unless specified + /// + public virtual DistributedCacheDefaults DefaultCachePolicy { get; set; } = new DistributedCacheDefaults(); + + public virtual void Add(string key, T item, DistributedCacheEntryOptions policy) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + ValidateKey(key); + + DistributedCacheProvider.Set(key, item, policy); + } + + public virtual T Get(string key) + { + ValidateKey(key); + + var item = DistributedCacheProvider.Get(key); + + return GetValueFromLazy(item); + } + + public virtual Task GetAsync(string key) + { + ValidateKey(key); + + var item = DistributedCacheProvider.Get(key); + + return GetValueFromAsyncLazy(item); + } + + public virtual T GetOrAdd(string key, Func addItemFactory) + { + ValidateKey(key); + + object cacheItem; + locker.Wait(); //TODO: do we really need this? Could we just lock on the key? + + try + { + var value = (T)DistributedCacheProvider.GetOrCreate(key, addItemFactory); + cacheItem = new Lazy(() => value); + } + finally + { + locker.Release(); + } + + try + { + return GetValueFromLazy(cacheItem); + } + catch //addItemFactory errored so do not cache the exception + { + DistributedCacheProvider.Remove(key); + throw; + } + } + + public virtual void Remove(string key) + { + ValidateKey(key); + DistributedCacheProvider.Remove(key); + } + + + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory) + { + ValidateKey(key); + + object cacheItem; + + // 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 + + await locker.WaitAsync() + .ConfigureAwait( + false); //TODO: do we really need to lock everything here - faster if we could lock on just the key? + try + { + var value = await DistributedCacheProvider.GetOrCreateAsync(key, addItemFactory); + cacheItem = new Lazy(() => (T) value); + } + finally + { + locker.Release(); + } + + try + { + var result = GetValueFromAsyncLazy(cacheItem); + + if (result.IsCanceled || result.IsFaulted) + DistributedCacheProvider.Remove(key); + + return await result.ConfigureAwait(false); + } + catch //addItemFactory errored so do not cache the exception + { + DistributedCacheProvider.Remove(key); + throw; + } + } + + protected virtual T GetValueFromLazy(object item) + { + 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 GetValueFromAsyncLazy(object item) + { + 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)); + } + + protected virtual void ValidateKey(string key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentOutOfRangeException(nameof(key), "Cache keys cannot be empty or whitespace"); + } + } +} \ No newline at end of file diff --git a/LazyCache/IDistributedAppCache.cs b/LazyCache/IDistributedAppCache.cs new file mode 100644 index 0000000..ab56443 --- /dev/null +++ b/LazyCache/IDistributedAppCache.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; + +namespace LazyCache +{ + public interface IDistributedAppCache + { + IDistributedCacheProvider DistributedCacheProvider { get; } + + /// + /// Define the number of seconds to cache objects for by default + /// + DistributedCacheDefaults DefaultCachePolicy { get; } + + void Add(string key, T item, DistributedCacheEntryOptions policy); + + T Get(string key); + + T GetOrAdd(string key, Func addItemFactory); + + Task GetAsync(string key); + + Task GetOrAddAsync(string key, Func> addItemFactory); + + void Remove(string key); + } +} \ No newline at end of file diff --git a/LazyCache/IDistributedCacheProvider.cs b/LazyCache/IDistributedCacheProvider.cs new file mode 100644 index 0000000..b8a16d2 --- /dev/null +++ b/LazyCache/IDistributedCacheProvider.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; + +public interface IDistributedCacheProvider +{ + object Get(string key); + T Get(string key); + void Set(string key, object item, DistributedCacheEntryOptions policy); + object GetOrCreate(string key, Func func); + Task GetOrCreateAsync(string key, Func> func); + void Remove(string key); +} \ No newline at end of file diff --git a/LazyCache/LazyCache.csproj b/LazyCache/LazyCache.csproj index e412b30..af41258 100644 --- a/LazyCache/LazyCache.csproj +++ b/LazyCache/LazyCache.csproj @@ -25,6 +25,9 @@ + + + diff --git a/LazyCache/Providers/DistributedCacheProvider.cs b/LazyCache/Providers/DistributedCacheProvider.cs new file mode 100644 index 0000000..59cf34d --- /dev/null +++ b/LazyCache/Providers/DistributedCacheProvider.cs @@ -0,0 +1,120 @@ +using Microsoft.Extensions.Caching.Distributed; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using System; +using System.IO; +using System.Threading.Tasks; + + +namespace LazyCache.Providers +{ + public class DistributedCacheProvider : IDistributedCacheProvider + { + internal readonly IDistributedCache cache; + + internal readonly JsonSerializerSettings deserializerSettings = new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Serialize, + PreserveReferencesHandling = PreserveReferencesHandling.All, + TypeNameHandling = TypeNameHandling.All + }; + + public DistributedCacheProvider(IDistributedCache cache) + { + this.cache = cache; + } + + internal object Set(DistributedCacheEntry entry) + { + cache.SetString(entry.Key, JsonConvert.SerializeObject(entry.Value, deserializerSettings), entry.DistributedCacheEntryOptions); + return entry.Value; + } + + internal async Task SetAsync(DistributedCacheEntry entry) + { + await cache.SetStringAsync(entry.Key, JsonConvert.SerializeObject(entry.Value, deserializerSettings), entry.DistributedCacheEntryOptions); + } + + public void Set(string key, object item, DistributedCacheEntryOptions policy) + { + cache.SetString(key, JsonConvert.SerializeObject(item, deserializerSettings), policy); + } + + private static string ToBson(T value) + { + using (MemoryStream ms = new MemoryStream()) + using (BsonDataWriter datawriter = new BsonDataWriter(ms)) + { + JsonSerializer serializer = new JsonSerializer(); + serializer.Serialize(datawriter, value); + return Convert.ToBase64String(ms.ToArray()); + } + } + + private static T FromBson(string base64data) + { + byte[] data = Convert.FromBase64String(base64data); + + using (MemoryStream ms = new MemoryStream(data)) + using (BsonDataReader reader = new BsonDataReader(ms)) + { + JsonSerializer serializer = new JsonSerializer(); + return serializer.Deserialize(reader); + } + } + + public T Get(string key) + { + var valueJson = cache.GetString(key); + if (valueJson == null) + return default(T); + return JsonConvert.DeserializeObject(valueJson, deserializerSettings); + } + + public object Get(string key) + { + var valueJson = cache.GetString(key); + if (valueJson == null) + return null; + return JsonConvert.DeserializeObject(valueJson, deserializerSettings); + } + + public object GetOrCreate(string key, Func func) + { + if (!TryGetValue(key, out T result)) + { + var entry = new DistributedCacheEntry(key); + result = func(entry); + entry.SetValue(result); + Set(entry); + } + + return result; + } + + public async Task GetOrCreateAsync(string key, Func> func) + { + if (!TryGetValue(key, out T result)) + { + var entry = new DistributedCacheEntry(key); + result = func(entry).GetAwaiter().GetResult(); + entry.SetValue(result); + + await SetAsync(entry); + } + + return result; + } + + public void Remove(string key) + { + cache.Remove(key); + } + + private bool TryGetValue(string key, out T value) + { + value = Get(key); + return value != null && !value.Equals(default(T)); + } + } +} \ No newline at end of file From dbc06eda7dd2dac0ae51faa6b7fb69c723dcd24f Mon Sep 17 00:00:00 2001 From: adobarganes1 Date: Fri, 22 Mar 2019 15:48:19 -0400 Subject: [PATCH 2/4] added distributed cache extensions --- ...tedLazyCacheServiceCollectionExtensions.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 LazyCache.AspNetCore/DistributedLazyCacheServiceCollectionExtensions.cs diff --git a/LazyCache.AspNetCore/DistributedLazyCacheServiceCollectionExtensions.cs b/LazyCache.AspNetCore/DistributedLazyCacheServiceCollectionExtensions.cs new file mode 100644 index 0000000..d49010e --- /dev/null +++ b/LazyCache.AspNetCore/DistributedLazyCacheServiceCollectionExtensions.cs @@ -0,0 +1,40 @@ +using System; +using LazyCache; +using LazyCache.Providers; +using Microsoft.Extensions.Caching.Distributed; +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 DistributedLazyCacheServiceCollectionExtensions + { + public static IServiceCollection AddDistributedLazyCache(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 AddDistributedLazyCache(this IServiceCollection services, + Func implementationFactory) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + if (implementationFactory == null) throw new ArgumentNullException(nameof(implementationFactory)); + + services.AddOptions(); + services.TryAdd(ServiceDescriptor.Singleton()); + + services.TryAdd(ServiceDescriptor.Singleton(implementationFactory)); + + return services; + } + } +} From 0a4852eaa0ec8ad0d182dbe0e7838de653cf98aa Mon Sep 17 00:00:00 2001 From: adobarganes1 Date: Mon, 25 Mar 2019 16:59:57 -0400 Subject: [PATCH 3/4] created decorator HybridCacheProvider --- .../DistributedCachingServiceProviderTests.cs | 48 ++-- LazyCache/DistributedCachingServiceV2.cs | 215 ++++++++++++++++++ .../Providers/DistributedCacheProvider.cs | 57 +++-- LazyCache/Providers/HybridCacheProvider.cs | 69 ++++++ 4 files changed, 342 insertions(+), 47 deletions(-) create mode 100644 LazyCache/DistributedCachingServiceV2.cs create mode 100644 LazyCache/Providers/HybridCacheProvider.cs diff --git a/LazyCache.UnitTests/DistributedCachingServiceProviderTests.cs b/LazyCache.UnitTests/DistributedCachingServiceProviderTests.cs index 8f65294..b57cd6e 100644 --- a/LazyCache.UnitTests/DistributedCachingServiceProviderTests.cs +++ b/LazyCache.UnitTests/DistributedCachingServiceProviderTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using MongoDB.Bson; using NUnit.Framework; namespace LazyCache.UnitTests @@ -21,12 +22,13 @@ public void BeforeEachTest() testObject = new ComplexTestObject(); } - private static DistributedCachingService BuildCache() + private static HybridCachingService BuildCache() { var options = new MemoryDistributedCacheOptions(); IDistributedCache memoryDistributedCache = new MemoryDistributedCache(Options.Create(options)); IDistributedCacheProvider distributedCacheProvider = new DistributedCacheProvider(memoryDistributedCache); - return new DistributedCachingService(distributedCacheProvider); + IDistributedCacheProvider hybridCacheProvider = new HybridCacheProvider(distributedCacheProvider, new MemoryCache(Options.Create(new MemoryCacheOptions()))); + return new HybridCachingService(hybridCacheProvider); } private IDistributedAppCache sut; @@ -322,7 +324,7 @@ public async Task GetOrAddAsyncAndThenGetAsyncObjectReturnsCorrectType() await sut.GetOrAddAsync(TestKey, () => Task.FromResult(testObject)); var actual = await sut.GetAsync(TestKey); Assert.IsNotNull(actual); - Assert.That(actual, Is.EqualTo(testObject)); + Assert.AreEqual(actual.ToJson(), testObject.ToJson()); } [Test] @@ -350,12 +352,12 @@ Task FetchAsync() var actualAsync = await sut.GetOrAddAsync(TestKey, FetchAsync); Assert.IsNotNull(actualSync); - Assert.That(actualSync, Is.EqualTo(testObject)); + Assert.That(actualSync.ToJson(), Is.EqualTo(testObject.ToJson())); Assert.IsNotNull(actualAsync); - Assert.That(actualAsync, Is.EqualTo(testObject)); + Assert.That(actualAsync.ToJson(), Is.EqualTo(testObject.ToJson())); - Assert.AreEqual(actualAsync, actualSync); + Assert.AreEqual(actualAsync.ToJson(), actualSync.ToJson()); } [Test] @@ -367,15 +369,15 @@ public async Task GetOrAddAsyncTaskAndThenGetTaskOfAnotherTypeReturnsNull() Assert.Null(actual); } - [Test] - public async Task GetOrAddAsyncTaskAndThenGetTaskOfObjectReturnsCorrectType() - { - var cachedAsyncResult = testObject; - await sut.GetOrAddAsync(TestKey, () => Task.FromResult(cachedAsyncResult)); - var actual = sut.Get>(TestKey); - Assert.IsNotNull(actual); - Assert.That(actual.Result, Is.EqualTo(cachedAsyncResult)); - } + //[Test] + //public async Task GetOrAddAsyncTaskAndThenGetTaskOfObjectReturnsCorrectType() + //{ + // var cachedAsyncResult = testObject; + // await sut.GetOrAddAsync(TestKey, () => Task.FromResult(cachedAsyncResult)); + // var actual = sut.Get>(TestKey); + // Assert.IsNotNull(actual); + // Assert.That(actual.Result, Is.EqualTo(cachedAsyncResult)); + //} [Test] public async Task GetOrAddAsyncWillAddOnFirstCall() @@ -439,7 +441,7 @@ public void GetOrAddAsyncWithALongTaskReturnsBeforeTaskCompletes() Task FetchAsync() { - return Task.Delay(TimeSpan.FromMinutes(1)) + return Task.Delay(TimeSpan.FromSeconds(5)) .ContinueWith(x => cachedResult); } @@ -457,7 +459,7 @@ public async Task GetOrAddAsyncWithOffsetWillAddAndReturnTaskOfCached() () => Task.FromResult(new DateTime(2001, 01, 01)), DateTimeOffset.Now.AddSeconds(5) ); - var expectedSecond = await sut.Get>(TestKey); + var expectedSecond = await sut.GetAsync(TestKey); Assert.AreEqual(2001, expectedFirst.Year); Assert.AreEqual(2001, expectedSecond.Year); @@ -469,8 +471,8 @@ public async Task GetOrAddAsyncWithPolicyAndThenGetTaskObjectReturnsCorrectType( var item = testObject; await sut.GetOrAddAsync(TestKey, () => Task.FromResult(item), oneHourNonRemoveableDistributedCacheEntryOptions); - var actual = await sut.Get>(TestKey); - Assert.That(actual, Is.EqualTo(item)); + var actual = await sut.GetAsync(TestKey); + Assert.That(actual.ToBson(), Is.EqualTo(item.ToBson())); } @@ -507,12 +509,12 @@ ComplexTestObject FetchSync() var actualSync = sut.GetOrAdd(TestKey, FetchSync); Assert.IsNotNull(actualAsync); - Assert.That(actualAsync, Is.EqualTo(testObject)); + Assert.That(actualAsync.ToBson(), Is.EqualTo(testObject.ToBson())); Assert.IsNotNull(actualSync); - Assert.That(actualSync, Is.EqualTo(testObject)); + Assert.That(actualSync.ToBson(), Is.EqualTo(testObject.ToBson())); - Assert.AreEqual(actualAsync, actualSync); + Assert.AreEqual(actualAsync.ToBson(), actualSync.ToBson()); } [Test] @@ -635,7 +637,7 @@ public void GetWithClassTypeParamReturnsType() { var cached = new EventArgs(); sut.Add(TestKey, cached); - Assert.AreEqual(cached, sut.Get(TestKey)); + Assert.AreEqual(cached.ToJson(), sut.Get(TestKey).ToJson()); } [Test] diff --git a/LazyCache/DistributedCachingServiceV2.cs b/LazyCache/DistributedCachingServiceV2.cs new file mode 100644 index 0000000..2a52dac --- /dev/null +++ b/LazyCache/DistributedCachingServiceV2.cs @@ -0,0 +1,215 @@ +using Microsoft.Extensions.Caching.Distributed; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace LazyCache +{ + public class HybridCachingService : IDistributedAppCache + { + private readonly Lazy cacheProvider; + + private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); + + public HybridCachingService(Lazy cacheProvider) + { + this.cacheProvider = cacheProvider ?? throw new ArgumentNullException(nameof(cacheProvider)); + } + + public HybridCachingService(Func cacheProviderFactory) + { + if (cacheProviderFactory == null) throw new ArgumentNullException(nameof(cacheProviderFactory)); + cacheProvider = new Lazy(cacheProviderFactory); + } + + public HybridCachingService(IDistributedCacheProvider cache) : this(() => cache) + { + if (cache == null) throw new ArgumentNullException(nameof(cache)); + } + + /// + /// Seconds to cache objects for by default + /// + [Obsolete("DefaultCacheDuration has been replaced with DefaultCacheDurationSeconds")] + public virtual int DefaultCacheDuration + { + get => DefaultCachePolicy.DefaultCacheDurationSeconds; + set => DefaultCachePolicy.DefaultCacheDurationSeconds = value; + } + + public virtual IDistributedCacheProvider DistributedCacheProvider => cacheProvider.Value; + + /// + /// Policy defining how long items should be cached for unless specified + /// + public virtual DistributedCacheDefaults DefaultCachePolicy { get; set; } = new DistributedCacheDefaults(); + + public virtual void Add(string key, T item, DistributedCacheEntryOptions policy) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + ValidateKey(key); + + DistributedCacheProvider.Set(key, item, policy); + } + + public virtual T Get(string key) + { + ValidateKey(key); + + var item = DistributedCacheProvider.Get(key); + + return GetValueFromLazy(item); + } + + public virtual Task GetAsync(string key) + { + ValidateKey(key); + + var item = DistributedCacheProvider.Get(key); + + return GetValueFromAsyncLazy(item); + } + + public virtual T GetOrAdd(string key, Func addItemFactory) + { + ValidateKey(key); + DistributedCacheEntry temporaryCacheEntry = null; + object cacheItem; + locker.Wait(); //TODO: do we really need this? Could we just lock on the key? + + try + { + cacheItem = DistributedCacheProvider.GetOrCreate(key, entry => + new Lazy(() => + { + temporaryCacheEntry = entry; + var result = addItemFactory(entry); + return result; + }) + ); + } + finally + { + locker.Release(); + } + + try + { + var toBeCached = GetValueFromLazy(cacheItem); + DistributedCacheProvider.Set(key, toBeCached, temporaryCacheEntry != null ? temporaryCacheEntry.DistributedCacheEntryOptions : DefaultCachePolicy.BuildOptions()); + return toBeCached; + } + catch //addItemFactory errored so do not cache the exception + { + DistributedCacheProvider.Remove(key); + throw; + } + } + + public virtual void Remove(string key) + { + ValidateKey(key); + DistributedCacheProvider.Remove(key); + } + + + public virtual async Task GetOrAddAsync(string key, Func> addItemFactory) + { + ValidateKey(key); + + object cacheItem; + DistributedCacheEntry temporaryCacheEntry = null; + // 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 + + await locker.WaitAsync() + .ConfigureAwait( + false); //TODO: do we really need to lock everything here - faster if we could lock on just the key? + try + { + // var value = await DistributedCacheProvider.GetOrCreateAsync(key, addItemFactory); + // cacheItem = new Lazy(() => (T) value); + + cacheItem = DistributedCacheProvider.GetOrCreate(key, entry => + new AsyncLazy(() => + { + temporaryCacheEntry = entry; + var result = addItemFactory(entry); + return result; + }) + ); + } + finally + { + locker.Release(); + } + + try + { + var result = GetValueFromAsyncLazy(cacheItem); + + if (result.IsCanceled || result.IsFaulted) + DistributedCacheProvider.Remove(key); + + var toBeCached = await result.ConfigureAwait(false); + DistributedCacheProvider.Set(key, toBeCached, temporaryCacheEntry != null ? temporaryCacheEntry.DistributedCacheEntryOptions : DefaultCachePolicy.BuildOptions()); + return toBeCached; + } + catch //addItemFactory errored so do not cache the exception + { + DistributedCacheProvider.Remove(key); + throw; + } + } + + protected virtual T GetValueFromLazy(object item) + { + 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 GetValueFromAsyncLazy(object item) + { + 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)); + } + + protected virtual void ValidateKey(string key) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentOutOfRangeException(nameof(key), "Cache keys cannot be empty or whitespace"); + } + } +} \ No newline at end of file diff --git a/LazyCache/Providers/DistributedCacheProvider.cs b/LazyCache/Providers/DistributedCacheProvider.cs index 59cf34d..b929e59 100644 --- a/LazyCache/Providers/DistributedCacheProvider.cs +++ b/LazyCache/Providers/DistributedCacheProvider.cs @@ -40,35 +40,44 @@ public void Set(string key, object item, DistributedCacheEntryOptions policy) cache.SetString(key, JsonConvert.SerializeObject(item, deserializerSettings), policy); } - private static string ToBson(T value) - { - using (MemoryStream ms = new MemoryStream()) - using (BsonDataWriter datawriter = new BsonDataWriter(ms)) - { - JsonSerializer serializer = new JsonSerializer(); - serializer.Serialize(datawriter, value); - return Convert.ToBase64String(ms.ToArray()); - } - } - - private static T FromBson(string base64data) - { - byte[] data = Convert.FromBase64String(base64data); - - using (MemoryStream ms = new MemoryStream(data)) - using (BsonDataReader reader = new BsonDataReader(ms)) - { - JsonSerializer serializer = new JsonSerializer(); - return serializer.Deserialize(reader); - } - } + //private static string ToBson(T value) + //{ + // using (MemoryStream ms = new MemoryStream()) + // using (BsonDataWriter datawriter = new BsonDataWriter(ms)) + // { + // JsonSerializer serializer = new JsonSerializer(); + // serializer.Serialize(datawriter, value); + // return Convert.ToBase64String(ms.ToArray()); + // } + //} + + //private static T FromBson(string base64data) + //{ + // byte[] data = Convert.FromBase64String(base64data); + + // using (MemoryStream ms = new MemoryStream(data)) + // using (BsonDataReader reader = new BsonDataReader(ms)) + // { + // JsonSerializer serializer = new JsonSerializer(); + // return serializer.Deserialize(reader); + // } + //} public T Get(string key) { + var cachedItem = default(T); + var valueJson = cache.GetString(key); if (valueJson == null) - return default(T); - return JsonConvert.DeserializeObject(valueJson, deserializerSettings); + return cachedItem; + try + { + return JsonConvert.DeserializeObject(valueJson, deserializerSettings); + } + catch (Exception e) + { + return cachedItem; + } } public object Get(string key) diff --git a/LazyCache/Providers/HybridCacheProvider.cs b/LazyCache/Providers/HybridCacheProvider.cs new file mode 100644 index 0000000..c6f57f0 --- /dev/null +++ b/LazyCache/Providers/HybridCacheProvider.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; +using System; +using System.IO; +using System.Threading.Tasks; + + +namespace LazyCache.Providers +{ + public class HybridCacheProvider : IDistributedCacheProvider + { + private readonly IDistributedCacheProvider distributedCacheProvider; + private readonly IMemoryCache memoryCache; + + public HybridCacheProvider(IDistributedCacheProvider distributedCacheProvider, IMemoryCache memoryCache) + { + this.distributedCacheProvider = distributedCacheProvider; + this.memoryCache = memoryCache; + } + + public void Set(string key, object item, DistributedCacheEntryOptions policy) + { + distributedCacheProvider.Set(key, item, policy); + } + + public T Get(string key) + { + return distributedCacheProvider.Get(key); + } + + public object Get(string key) + { + return distributedCacheProvider.Get(key); + } + + public object GetOrCreate(string key, Func func) + { + if (!TryGetValue(key, out T result)) + { + return memoryCache.GetOrCreate(key, (e) => func(new DistributedCacheEntry(key))); + } + + return result; + } + + public async Task GetOrCreateAsync(string key, Func> func) + { + if (!TryGetValue(key, out T result)) + { + return await memoryCache.GetOrCreateAsync(key, (e) => func(new DistributedCacheEntry(key))); + } + + return result; + } + + public void Remove(string key) + { + distributedCacheProvider.Remove(key); + } + + private bool TryGetValue(string key, out T value) + { + value = Get(key); + return value != null && !value.Equals(default(T)); + } + } +} \ No newline at end of file From 51602e1f0d38cf7dd020d0cf57ae6425a74a5292 Mon Sep 17 00:00:00 2001 From: adobarganes1 Date: Mon, 25 Mar 2019 18:50:59 -0400 Subject: [PATCH 4/4] added sample to web project added sample to console project added hybrid extensions --- .../DbTimeDistributedController.cs | 40 +++++ .../DbTimeContext.cs | 2 +- CacheDatabaseQueriesApiSample/Startup.cs | 11 +- .../wwwroot/index-d.html | 159 ++++++++++++++++++ Console.Net461/Program.cs | 9 + ...tedLazyCacheServiceCollectionExtensions.cs | 40 ++++- ...ngServiceV2.cs => HybridCachingService.cs} | 0 LazyCache/LazyCache.csproj | 1 + LazyCache/Providers/HybridCacheProvider.cs | 1 + 9 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 CacheDatabaseQueriesApiSample/Controllers/DbTimeDistributedController.cs create mode 100644 CacheDatabaseQueriesApiSample/wwwroot/index-d.html rename LazyCache/{DistributedCachingServiceV2.cs => HybridCachingService.cs} (100%) diff --git a/CacheDatabaseQueriesApiSample/Controllers/DbTimeDistributedController.cs b/CacheDatabaseQueriesApiSample/Controllers/DbTimeDistributedController.cs new file mode 100644 index 0000000..9687e79 --- /dev/null +++ b/CacheDatabaseQueriesApiSample/Controllers/DbTimeDistributedController.cs @@ -0,0 +1,40 @@ +using System; +using LazyCache; +using Microsoft.AspNetCore.Mvc; + +namespace CacheDatabaseQueriesApiSample.Controllers +{ + public class DbTimeDistributedController : Controller + { + private readonly IDistributedAppCache _distributedCache; + private readonly string cacheKey = "DbTimeController.Get"; + private readonly DbTimeContext dbContext; + + + public DbTimeDistributedController(DbTimeContext context, IDistributedAppCache distributedCache) + { + dbContext = context; + _distributedCache = distributedCache; + } + + [HttpGet] + [Route("api/ddbtime")] + public DbTimeEntity Get() + { + Func actionThatWeWantToCache = () => dbContext.GeDbTime(); + + var cachedDatabaseTime = _distributedCache.GetOrAdd(cacheKey, actionThatWeWantToCache); + + return cachedDatabaseTime; + } + + [HttpDelete] + [Route("api/ddbtime")] + public IActionResult DeleteFromCache() + { + _distributedCache.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/CacheDatabaseQueriesApiSample/DbTimeContext.cs b/CacheDatabaseQueriesApiSample/DbTimeContext.cs index 0990351..5f20c5e 100644 --- a/CacheDatabaseQueriesApiSample/DbTimeContext.cs +++ b/CacheDatabaseQueriesApiSample/DbTimeContext.cs @@ -25,7 +25,7 @@ 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]") - .Single(); + .SingleOrDefault(); databaseRequestCounter++; diff --git a/CacheDatabaseQueriesApiSample/Startup.cs b/CacheDatabaseQueriesApiSample/Startup.cs index e1a3044..15b1da1 100644 --- a/CacheDatabaseQueriesApiSample/Startup.cs +++ b/CacheDatabaseQueriesApiSample/Startup.cs @@ -1,6 +1,9 @@ -using Microsoft.AspNetCore.Builder; +using LazyCache; +using LazyCache.Providers; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -29,6 +32,12 @@ public void ConfigureServices(IServiceCollection services) // Register IAppCache as a singleton CachingService services.AddLazyCache(); + + services.AddMemoryCache(); + services.AddDistributedMemoryCache(); + services.AddSingleton(); + services.AddTransient(provider => new HybridCacheProvider(provider.GetRequiredService(), provider.GetRequiredService())); + services.AddDistributedHybridLazyCache(provider => new HybridCachingService(provider.GetRequiredService())); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/CacheDatabaseQueriesApiSample/wwwroot/index-d.html b/CacheDatabaseQueriesApiSample/wwwroot/index-d.html new file mode 100644 index 0000000..01d5e09 --- /dev/null +++ b/CacheDatabaseQueriesApiSample/wwwroot/index-d.html @@ -0,0 +1,159 @@ + + + + + + + Lazy cache sample app + + + + + +
+
+
0 Database query(s)
+ +

Sample app to demonstrate using a distributed 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 + result is cached on the server on the first call, the time stays the same untill you clear + the cache and no more SQL queries are made.
+
+ + After you clear the cache you should see the time change because a real SQL query is made.
+
+ + Also note how real SQL queryies are slower than cache hits. +

+ +
+ +
+ + +

+
+
+ + + + + + + \ No newline at end of file diff --git a/Console.Net461/Program.cs b/Console.Net461/Program.cs index 94df07c..eceb75c 100644 --- a/Console.Net461/Program.cs +++ b/Console.Net461/Program.cs @@ -4,6 +4,10 @@ using System.Text; using System.Threading.Tasks; using LazyCache; +using LazyCache.Providers; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; using Ninject; namespace Console.Net461 @@ -26,6 +30,11 @@ static void Main(string[] args) item = cache.GetOrAdd("Program.Main.Person", () => Tuple.Create("Joe Blogs", DateTime.UtcNow)); System.Console.WriteLine(item.Item1); + + IDistributedAppCache distributedCache = new HybridCachingService(new HybridCacheProvider(new DistributedCacheProvider(new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()))), new MemoryCache(Options.Create(new MemoryCacheOptions())))); + item = distributedCache.GetOrAdd("Program.Main.Person", () => Tuple.Create("Joe Blogs", DateTime.UtcNow)); + + System.Console.WriteLine(item.Item1); } } } diff --git a/LazyCache.AspNetCore/DistributedLazyCacheServiceCollectionExtensions.cs b/LazyCache.AspNetCore/DistributedLazyCacheServiceCollectionExtensions.cs index d49010e..416a8ff 100644 --- a/LazyCache.AspNetCore/DistributedLazyCacheServiceCollectionExtensions.cs +++ b/LazyCache.AspNetCore/DistributedLazyCacheServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using LazyCache; using LazyCache.Providers; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection.Extensions; // ReSharper disable once CheckNamespace - MS guidelines say put DI registration in this NS @@ -15,10 +16,9 @@ public static IServiceCollection AddDistributedLazyCache(this IServiceCollection if (services == null) throw new ArgumentNullException(nameof(services)); services.AddOptions(); - services.TryAdd(ServiceDescriptor.Singleton()); - services.TryAdd(ServiceDescriptor.Singleton()); - - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); return services; } @@ -30,9 +30,37 @@ public static IServiceCollection AddDistributedLazyCache(this IServiceCollection if (implementationFactory == null) throw new ArgumentNullException(nameof(implementationFactory)); services.AddOptions(); - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAddSingleton(); + services.TryAddSingleton(implementationFactory); + + return services; + } + + public static IServiceCollection AddDistributedHybridLazyCache(this IServiceCollection services) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + + services.AddOptions(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(provider => new HybridCacheProvider(provider.GetRequiredService(), provider.GetRequiredService())); + services.TryAddSingleton(); - services.TryAdd(ServiceDescriptor.Singleton(implementationFactory)); + return services; + } + + public static IServiceCollection AddDistributedHybridLazyCache(this IServiceCollection services, + Func implementationFactory) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + if (implementationFactory == null) throw new ArgumentNullException(nameof(implementationFactory)); + + services.AddOptions(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(provider => new HybridCacheProvider(provider.GetRequiredService(), provider.GetRequiredService())); + services.TryAddSingleton(implementationFactory); return services; } diff --git a/LazyCache/DistributedCachingServiceV2.cs b/LazyCache/HybridCachingService.cs similarity index 100% rename from LazyCache/DistributedCachingServiceV2.cs rename to LazyCache/HybridCachingService.cs diff --git a/LazyCache/LazyCache.csproj b/LazyCache/LazyCache.csproj index af41258..94b96e6 100644 --- a/LazyCache/LazyCache.csproj +++ b/LazyCache/LazyCache.csproj @@ -2,6 +2,7 @@ + true netstandard2.0 true 1.0.0 diff --git a/LazyCache/Providers/HybridCacheProvider.cs b/LazyCache/Providers/HybridCacheProvider.cs index c6f57f0..5aac7da 100644 --- a/LazyCache/Providers/HybridCacheProvider.cs +++ b/LazyCache/Providers/HybridCacheProvider.cs @@ -58,6 +58,7 @@ public async Task GetOrCreateAsync(string key, Func(string key, out T value)