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
@@ -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/Console.Net461/App.config b/Console.Net461/App.config
new file mode 100644
index 0000000..c244acc
--- /dev/null
+++ b/Console.Net461/App.config
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..c16cd8a
--- /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.5.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.2\lib\netstandard2.0\System.Memory.dll
+
+
+
+ ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll
+
+
+ ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\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/LazyCache.UnitTests/Properties/AssemblyInfo.cs b/Console.Net461/Properties/AssemblyInfo.cs
similarity index 72%
rename from LazyCache.UnitTests/Properties/AssemblyInfo.cs
rename to Console.Net461/Properties/AssemblyInfo.cs
index f0e46fc..593ecab 100644
--- a/LazyCache.UnitTests/Properties/AssemblyInfo.cs
+++ b/Console.Net461/Properties/AssemblyInfo.cs
@@ -1,37 +1,36 @@
-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
+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..d68252b
--- /dev/null
+++ b/Console.Net461/packages.config
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj
new file mode 100644
index 0000000..2939774
--- /dev/null
+++ b/LazyCache.AspNetCore/LazyCache.AspNetCore.csproj
@@ -0,0 +1,33 @@
+
+
+
+
+ netstandard2.0
+ LazyCache
+ true
+ 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
+ Copyright 2014 - 2018 Alastair Crabtree
+ 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.AspNetCore/LazyCacheServiceCollectionExtensions.cs b/LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs
new file mode 100644
index 0000000..3a5643e
--- /dev/null
+++ b/LazyCache.AspNetCore/LazyCacheServiceCollectionExtensions.cs
@@ -0,0 +1,41 @@
+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;
+ }
+ }
+}
\ No newline at end of file
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/CachingServiceMemoryCacheProviderTests.cs
similarity index 67%
rename from LazyCache.UnitTests/CachingServiceTests.cs
rename to LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs
index d20ea1a..85e5e8b 100644
--- a/LazyCache.UnitTests/CachingServiceTests.cs
+++ b/LazyCache.UnitTests/CachingServiceMemoryCacheProviderTests.cs
@@ -1,43 +1,44 @@
using System;
using System.Collections.Generic;
-using System.Runtime.Caching;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
+using LazyCache.Providers;
+using Microsoft.Extensions.Caching.Memory;
using NUnit.Framework;
namespace LazyCache.UnitTests
{
[TestFixture]
- public class CachingServiceTests
+ public class CachingServiceMemoryCacheProviderTests
{
- [TearDown]
- public void TearDown()
- {
- MemoryCache.Default.Remove(TestKey);
- }
-
[SetUp]
public void BeforeEachTest()
{
- sut = new CachingService();
+ sut = BuildCache();
testObject = new ComplexTestObject();
}
- private CachingService sut;
-
- private readonly CacheItemPolicy oneHourNonRemoveableCacheItemPolicy = new CacheItemPolicy
+ private static CachingService BuildCache()
{
- AbsoluteExpiration = DateTimeOffset.Now.AddHours(1),
- Priority = CacheItemPriority.NotRemovable
- };
+ return new CachingService(new MemoryCacheProvider(new MemoryCache(new MemoryCacheOptions())));
+ }
+
+ private IAppCache sut;
+
+ private readonly MemoryCacheEntryOptions oneHourNonRemoveableMemoryCacheEntryOptions =
+ new MemoryCacheEntryOptions
+ {
+ AbsoluteExpiration = DateTimeOffset.Now.AddHours(1),
+ Priority = CacheItemPriority.NeverRemove
+ };
private ComplexTestObject testObject = new ComplexTestObject();
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";
@@ -46,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]
@@ -67,63 +72,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 +156,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));
}
@@ -170,6 +175,25 @@ public void AddWithSlidingThatExpiresReturnsNull()
Assert.IsNull(sut.Get(TestKey));
}
+ [Test]
+ public void CacheProviderIsNotNull()
+ {
+ sut.CacheProvider.Should().NotBeNull();
+ }
+
+ [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()
{
@@ -182,7 +206,7 @@ public void GetCachedNullableStructTypeParamReturnsType()
public void GetEmptyKeyThrowsException()
{
Action act = () => sut.Get("");
- act.ShouldThrow();
+ act.Should().Throw();
}
[Test]
@@ -217,60 +241,75 @@ public void GetFromCacheTwiceAtSameTimeOnlyAddsOnce()
public void GetNullKeyThrowsException()
{
Action act = () => sut.Get(null);
- act.ShouldThrow();
+ act.Should().Throw();
}
[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));
+ sut.GetOrAdd(TestKey, () => testObject);
+ var actual = sut.Get(TestKey);
+ Assert.IsNotNull(actual);
+ }
- Assert.IsNotNull(actualSync);
- Assert.That(actualSync, Is.EqualTo(testObject));
+ [Test]
+ public void GetOrAddAndThenGetValueObjectReturnsCorrectType()
+ {
+ sut.GetOrAdd(TestKey, () => 123);
+ var actual = sut.Get(TestKey);
+ Assert.AreEqual(123, actual);
+ }
- Assert.AreEqual(actualAsync, actualSync);
+ [Test]
+ public void GetOrAddAndThenGetWrongtypeObjectReturnsNull()
+ {
+ sut.GetOrAdd(TestKey, () => testObject);
+ 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);
+ 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()
{
- 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));
@@ -279,36 +318,41 @@ 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);
}
[Test]
- public void GetOrAddAndThenGetValueObjectReturnsCorrectType()
+ public async Task GetOrAddAsyncFollowinGetOrAddReturnsTheFirstObjectAndIgnoresTheSecondTask()
{
- Func fetch = () => 123;
- sut.GetOrAdd(TestKey, fetch);
- var actual = sut.Get(TestKey);
- Assert.AreEqual(123, actual);
- }
+ ComplexTestObject FetchSync()
+ {
+ return testObject;
+ }
- [Test]
- public void GetOrAddAndThenGetWrongtypeObjectReturnsNull()
- {
- Func fetch = () => testObject;
- sut.GetOrAdd(TestKey, fetch);
- var actual = sut.Get(TestKey);
- Assert.IsNull(actual);
+ 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;
- 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);
}
@@ -317,8 +361,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));
@@ -378,28 +421,22 @@ public async Task GetOrAddAsyncWillNotAddIfExistingData()
Assert.AreEqual(0, times);
}
- [Test, Timeout(20000)]
- public async Task GetOrAddAsyncWithCallbackOnRemovedReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand()
+ [Test]
+ [MaxTime(1000)]
+ public void GetOrAddAsyncWithALongTaskReturnsBeforeTaskCompletes()
{
- 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
- });
+ var cachedResult = testObject;
- sut.Remove(TestKey); //force removed callback to fire
- while (removedCallbackArgs == null)
- Thread.Sleep(500);
+ Task FetchAsync()
+ {
+ return Task.Delay(TimeSpan.FromMinutes(1))
+ .ContinueWith(x => cachedResult);
+ }
- var callbackResult = removedCallbackArgs.CacheItem.Value;
- Assert.That(callbackResult, Is.AssignableTo>());
- var callbackResultValue = await (Task) removedCallbackArgs.CacheItem.Value;
+ var actualResult = sut.GetOrAddAsync(TestKey, FetchAsync);
- Assert.AreEqual(123, callbackResultValue);
+ Assert.That(actualResult, Is.Not.Null);
+ Assert.That(actualResult.IsCompleted, Is.Not.True);
}
[Test]
@@ -409,7 +446,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);
@@ -420,24 +457,100 @@ public async Task GetOrAddAsyncWithOffsetWillAddAndReturnTaskOfCached()
public async Task GetOrAddAsyncWithPolicyAndThenGetTaskObjectReturnsCorrectType()
{
var item = testObject;
- Func> fetch = () => Task.FromResult(item);
- await sut.GetOrAddAsync(TestKey, fetch,
- oneHourNonRemoveableCacheItemPolicy);
+ await sut.GetOrAddAsync(TestKey, () => Task.FromResult(item),
+ oneHourNonRemoveableMemoryCacheEntryOptions);
var actual = await sut.Get>(TestKey);
Assert.That(actual, Is.EqualTo(item));
}
+ [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]
+ [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 (callbackValue == null)
+ Thread.Sleep(500);
+
+ 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);
- var actualResult = await sut.GetOrAddAsync(TestKey, fetchAsync, oneHourNonRemoveableCacheItemPolicy);
+ Task FetchAsync()
+ {
+ return Task.FromResult(cachedResult);
+ }
+
+ var actualResult =
+ await sut.GetOrAddAsync(TestKey, FetchAsync, oneHourNonRemoveableMemoryCacheEntryOptions);
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()
{
@@ -493,26 +606,6 @@ 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
- });
-
- sut.Remove(TestKey); //force removed callback to fire
- while (removedCallbackArgs == null)
- Thread.Sleep(500);
-
- Assert.AreEqual(123, removedCallbackArgs.CacheItem.Value);
- }
-
[Test]
public void GetOrAddWithOffsetWillAddAndReturnCached()
{
@@ -520,7 +613,7 @@ public void GetOrAddWithOffsetWillAddAndReturnCached()
TestKey,
() => new DateTime(2001, 01, 01),
DateTimeOffset.Now.AddSeconds(5)
- );
+ );
var expectedSecond = sut.Get(TestKey);
Assert.AreEqual(2001, expectedFirst.Year);
@@ -530,9 +623,8 @@ public void GetOrAddWithOffsetWillAddAndReturnCached()
[Test]
public void GetOrAddWithPolicyAndThenGetObjectReturnsCorrectType()
{
- Func fetch = () => testObject;
- sut.GetOrAdd(TestKey, fetch,
- oneHourNonRemoveableCacheItemPolicy);
+ sut.GetOrAdd(TestKey, () => testObject,
+ oneHourNonRemoveableMemoryCacheEntryOptions);
var actual = sut.Get(TestKey);
Assert.IsNotNull(actual);
}
@@ -540,8 +632,12 @@ public void GetOrAddWithPolicyAndThenGetObjectReturnsCorrectType()
[Test]
public void GetOrAddWithPolicyAndThenGetValueObjectReturnsCorrectType()
{
- Func fetch = () => 123;
- sut.GetOrAdd(TestKey, fetch, oneHourNonRemoveableCacheItemPolicy);
+ int Fetch()
+ {
+ return 123;
+ }
+
+ sut.GetOrAdd(TestKey, Fetch, oneHourNonRemoveableMemoryCacheEntryOptions);
var actual = sut.Get(TestKey);
Assert.AreEqual(123, actual);
}
@@ -556,40 +652,37 @@ 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()
+ [Test]
+ [MaxTime(20000)]
+ public void GetOrAddWithPostEvictionCallbackdReturnsTheOriginalCachedObjectEvenIfNotGettedBeforehand()
{
- 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);
+ 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)
+ while (cacheValue == null)
Thread.Sleep(500);
- Assert.AreEqual(123, removedCallbackArgs.CacheItem.Value);
+ cacheValue.Should().BeOfType();
+ cacheValue.Should().Be(123);
}
[Test]
@@ -662,54 +755,5 @@ public void RemovedItemCannotBeRetrievedFromCache()
sut.Remove(TestKey);
Assert.Null(sut.Get(TestKey));
}
-
- [Test]
- [Timeout(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.UnitTests/LazyCache.UnitTests.csproj b/LazyCache.UnitTests/LazyCache.UnitTests.csproj
index 77a9fcd..84c55c2 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.2
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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.sln b/LazyCache.sln
index a729d3f..00183dd 100644
--- a/LazyCache.sln
+++ b/LazyCache.sln
@@ -1,21 +1,27 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 14
-VisualStudioVersion = 14.0.25420.1
+# Visual Studio 15
+VisualStudioVersion = 15.0.27130.2036
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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CacheDatabaseQueriesApiSample", "CacheDatabaseQueriesApiSample\CacheDatabaseQueriesApiSample.csproj", "{5D6A88DD-230C-4057-B8EB-A987FF4F29DB}"
+EndProject
+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
@@ -31,15 +37,27 @@ 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
+ {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
+ {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
- {FB8EDC6D-CD41-47AA-9758-D8D796F7728C} = {335BA426-C839-4996-8476-F3EE4056C40E}
+ {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}
EndGlobalSection
EndGlobal
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/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/CacheDefaults.cs b/LazyCache/CacheDefaults.cs
new file mode 100644
index 0000000..f30b90a
--- /dev/null
+++ b/LazyCache/CacheDefaults.cs
@@ -0,0 +1,19 @@
+using System;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace LazyCache
+{
+ // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global
+ 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
new file mode 100644
index 0000000..be940fc
--- /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
diff --git a/LazyCache/CachingService.cs b/LazyCache/CachingService.cs
index ac59d88..1bd5305 100644
--- a/LazyCache/CachingService.cs
+++ b/LazyCache/CachingService.cs
@@ -1,242 +1,235 @@
using System;
-using System.Runtime.Caching;
+using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
+using LazyCache.Providers;
+using Microsoft.Extensions.Caching.Memory;
namespace LazyCache
{
+ // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global
public class CachingService : IAppCache
{
- public CachingService() : this(MemoryCache.Default)
+ private readonly Lazy cacheProvider;
+
+ private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1);
+
+ public CachingService() : this(DefaultCacheProvider)
{
}
- public CachingService(ObjectCache cache)
+ public CachingService(Lazy cacheProvider)
{
- if (cache == null)
- throw new ArgumentNullException(nameof(cache));
-
- ObjectCache = cache;
- DefaultCacheDuration = 60*20;
+ this.cacheProvider = cacheProvider ?? throw new ArgumentNullException(nameof(cacheProvider));
}
- ///
- /// Seconds to cache objects for by default
- ///
- public int DefaultCacheDuration { get; set; }
-
- private DateTimeOffset DefaultExpiryDateTime => DateTimeOffset.Now.AddSeconds(DefaultCacheDuration);
-
- public void Add(string key, T item)
+ public CachingService(Func cacheProviderFactory)
{
- Add(key, item, DefaultExpiryDateTime);
+ if (cacheProviderFactory == null) throw new ArgumentNullException(nameof(cacheProviderFactory));
+ cacheProvider = new Lazy(cacheProviderFactory);
}
- public void Add(string key, T item, DateTimeOffset expires)
+ public CachingService(ICacheProvider cache) : this(() => cache)
{
- Add(key, item, new CacheItemPolicy {AbsoluteExpiration = expires});
+ if (cache == null) throw new ArgumentNullException(nameof(cache));
}
- public void Add(string key, T item, TimeSpan slidingExpiration)
+ public static Lazy DefaultCacheProvider { get; set; }
+ = new Lazy(() =>
+ new MemoryCacheProvider(
+ new MemoryCache(
+ new MemoryCacheOptions())
+ ));
+
+ ///
+ /// Seconds to cache objects for by default
+ ///
+ [Obsolete("DefaultCacheDuration has been replaced with DefaultCacheDurationSeconds")]
+ public virtual int DefaultCacheDuration
{
- Add(key, item, new CacheItemPolicy {SlidingExpiration = slidingExpiration});
+ get => DefaultCachePolicy.DefaultCacheDurationSeconds;
+ set => DefaultCachePolicy.DefaultCacheDurationSeconds = value;
}
- public void Add(string key, T item, CacheItemPolicy policy)
+ ///
+ /// 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)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
ValidateKey(key);
- ObjectCache.Set(key, item, policy);
+ CacheProvider.Set(key, item, policy);
}
- public T Get(string key)
+ public virtual T Get(string key)
{
ValidateKey(key);
- var item = ObjectCache[key];
+ var item = CacheProvider.Get(key);
- return UnwrapLazy(item);
+ return GetValueFromLazy(item);
}
-
- public async Task GetAsync(string key)
+ public virtual Task GetAsync(string key)
{
ValidateKey(key);
- var item = ObjectCache[key];
-
- return await UnwrapAsyncLazys(item);
- }
-
+ var item = CacheProvider.Get(key);
- public T GetOrAdd(string key, Func addItemFactory)
- {
- return GetOrAdd(key, addItemFactory, DefaultExpiryDateTime);
+ return GetValueFromAsyncLazy(item);
}
-
- public async Task GetOrAddAsync(string key, Func> addItemFactory, CacheItemPolicy policy)
+ public virtual T GetOrAdd(string key, Func addItemFactory)
{
ValidateKey(key);
- var newLazyCacheItem = new AsyncLazy(addItemFactory);
-
- EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(policy);
-
- var existingCacheItem = ObjectCache.AddOrGetExisting(key, newLazyCacheItem, policy);
-
- if (existingCacheItem != null)
- return await UnwrapAsyncLazys(existingCacheItem);
-
+ object cacheItem;
+ locker.Wait(); //TODO: do we really need this? Could we just lock on the key?
try
{
- var result = newLazyCacheItem.Value;
-
- if (result.IsCanceled || result.IsFaulted)
- ObjectCache.Remove(key);
-
- return await result;
+ cacheItem = CacheProvider.GetOrCreate(key, entry =>
+ new Lazy(() =>
+ {
+ var result = addItemFactory(entry);
+ EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy(entry.PostEvictionCallbacks);
+ return result;
+ })
+ );
}
- catch //addItemFactory errored so do not cache the exception
+ finally
{
- ObjectCache.Remove(key);
- throw;
+ locker.Release();
}
- }
-
- public T GetOrAdd(string key, Func addItemFactory, DateTimeOffset expires)
- {
- return GetOrAdd(key, addItemFactory, new CacheItemPolicy {AbsoluteExpiration = expires});
- }
-
-
- public T GetOrAdd(string key, Func addItemFactory, TimeSpan slidingExpiration)
- {
- return GetOrAdd(key, addItemFactory, new CacheItemPolicy {SlidingExpiration = slidingExpiration});
- }
-
- public T GetOrAdd(string key, Func addItemFactory, CacheItemPolicy policy)
- {
- ValidateKey(key);
-
- var newLazyCacheItem = new Lazy(addItemFactory);
-
- EnsureRemovedCallbackDoesNotReturnTheLazy(policy);
-
- var existingCacheItem = ObjectCache.AddOrGetExisting(key, newLazyCacheItem, policy);
-
- if (existingCacheItem != null)
- return UnwrapLazy(existingCacheItem);
try
{
- return newLazyCacheItem.Value;
+ return GetValueFromLazy(cacheItem);
}
catch //addItemFactory errored so do not cache the exception
{
- ObjectCache.Remove(key);
+ CacheProvider.Remove(key);
throw;
}
}
-
- public void Remove(string key)
+ public virtual void Remove(string key)
{
ValidateKey(key);
- ObjectCache.Remove(key);
+ CacheProvider.Remove(key);
}
- public ObjectCache ObjectCache { 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);
- }
+ ValidateKey(key);
- public async Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires)
- {
- return await GetOrAddAsync(key, addItemFactory, new CacheItemPolicy {AbsoluteExpiration = expires});
- }
+ object cacheItem;
- public async Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration)
- {
- return await GetOrAddAsync(key, addItemFactory, new CacheItemPolicy {SlidingExpiration = slidingExpiration});
- }
-
- private static T UnwrapLazy(object item)
- {
- var lazy = item as Lazy;
- if (lazy != null)
- return lazy.Value;
+ // 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
- if (item is T)
- return (T) item;
+ await locker.WaitAsync()
+ .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 =>
+ new AsyncLazy(() =>
+ {
+ var result = addItemFactory(entry);
+ EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy(entry.PostEvictionCallbacks);
+ return result;
+ })
+ );
+ }
+ finally
+ {
+ locker.Release();
+ }
- var asyncLazy = item as AsyncLazy;
- if (asyncLazy != null)
- return asyncLazy.Value.Result;
+ try
+ {
+ var result = GetValueFromAsyncLazy(cacheItem);
- var task = item as Task;
- if (task != null)
- return task.Result;
+ if (result.IsCanceled || result.IsFaulted)
+ CacheProvider.Remove(key);
- return default(T);
+ return await result.ConfigureAwait(false);
+ }
+ catch //addItemFactory errored so do not cache the exception
+ {
+ CacheProvider.Remove(key);
+ throw;
+ }
}
- private static async Task UnwrapAsyncLazys(object item)
+ protected virtual T GetValueFromLazy(object item)
{
- var asyncLazy = item as AsyncLazy;
- if (asyncLazy != null)
- return await asyncLazy.Value;
-
- var task = item as Task;
- if (task != null)
- return await task;
-
- var lazy = item as Lazy;
- if (lazy != null)
- return lazy.Value;
-
- if (item is T)
- return (T) 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);
}
- private static void EnsureRemovedCallbackDoesNotReturnTheLazy(CacheItemPolicy policy)
+ protected virtual Task GetValueFromAsyncLazy(object item)
{
- if (policy?.RemovedCallback != null)
+ switch (item)
{
- var originallCallback = policy.RemovedCallback;
- policy.RemovedCallback = args =>
- {
- //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);
- };
+ 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));
}
- private static void EnsureRemovedCallbackDoesNotReturnTheAsyncLazy(CacheItemPolicy policy)
+ protected virtual void EnsureEvictionCallbackDoesNotReturnTheAsyncOrLazy(
+ IList callbackRegistrations)
{
- if (policy?.RemovedCallback != null)
- {
- var originallCallback = policy.RemovedCallback;
- policy.RemovedCallback = args =>
+ if (callbackRegistrations != null)
+ foreach (var item in callbackRegistrations)
{
- //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);
- };
- }
- }
-
- private void ValidateKey(string key)
+ var originalCallback = item.EvictionCallback;
+ item.EvictionCallback = (key, value, reason, state) =>
+ {
+ // 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);
+
+ // pass the unwrapped cached value to the original callback
+ originalCallback(key, value, reason, state);
+ };
+ }
+ }
+
+ 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 b4d65a8..b474cbe 100644
--- a/LazyCache/IAppCache.cs
+++ b/LazyCache/IAppCache.cs
@@ -1,30 +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; }
- 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);
+ ICacheProvider CacheProvider { get; }
- T Get(string key);
+ ///
+ /// Define the number of seconds to cache objects for by default
+ ///
+ CacheDefaults DefaultCachePolicy { get; }
- 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);
+ void Add(string key, T item, MemoryCacheEntryOptions policy);
- void Remove(string key);
+ T Get(string key);
+
+ T GetOrAdd(string key, Func addItemFactory);
- Task GetOrAddAsync(string key, Func> addItemFactory);
- Task GetOrAddAsync(string key, Func> addItemFactory, CacheItemPolicy policy);
- Task GetOrAddAsync(string key, Func> addItemFactory, DateTimeOffset expires);
- Task GetOrAddAsync(string key, Func> addItemFactory, TimeSpan slidingExpiration);
Task GetAsync