From 0742f7cef2ef2a3a6f56b13e4f51c215ea3adb33 Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Tue, 25 Oct 2016 10:16:38 +0200 Subject: [PATCH 01/14] Implemented item wrapper for http and memory cache --- build.fsx | 4 +- .../DoubleCache.SystemWebCaching/HttpCache.cs | 46 +++++++++++-------- source/DoubleCache/DoubleCache.csproj | 1 + source/DoubleCache/LocalCache/MemCache.cs | 1 + .../DoubleCacheTests/DoubleCacheTests.csproj | 1 + .../HttpCacheIntegrationTests.cs | 15 +++++- 6 files changed, 48 insertions(+), 20 deletions(-) diff --git a/build.fsx b/build.fsx index 9a6b6c3..0cf1721 100644 --- a/build.fsx +++ b/build.fsx @@ -13,7 +13,7 @@ let references = !! "source/DoubleCache/*.csproj" ++ "source/DoubleCache.SystemWebCaching/DoubleCache.SystemWebCaching.csproj" let testReferences = !! "source/DoubleCacheTests/*.csproj" -let version = "1.6.1" +let version = "2.0.0-beta.6" let commitHash = Information.getCurrentSHA1(".") let projectName = "DoubleCache" @@ -66,6 +66,7 @@ Target "CreateNuget" (fun _ -> WorkingDir = buildDir Version = version Publish = false + Prerelease = true Dependencies = [ "StackExchange.Redis", "1.0.488" "MsgPack.Cli", "0.6.5" @@ -85,6 +86,7 @@ Target "CreateNuget" (fun _ -> WorkingDir = buildDir Version = version Publish = false + Prerelease = true Dependencies = [ "DoubleCache", version ] diff --git a/source/DoubleCache.SystemWebCaching/HttpCache.cs b/source/DoubleCache.SystemWebCaching/HttpCache.cs index cf333e7..3e6f43e 100644 --- a/source/DoubleCache.SystemWebCaching/HttpCache.cs +++ b/source/DoubleCache.SystemWebCaching/HttpCache.cs @@ -1,14 +1,21 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Web.Caching; -namespace DoubleCache.SystemWebHttpCache +namespace DoubleCache.SystemWebCaching { public class HttpCache : ICacheAside { + internal class CacheItemWrapper + { + internal object Item { get; } + + internal CacheItemWrapper(object item) + { + Item = item; + } + } + private readonly Cache _cache; private readonly TimeSpan? _defaultTtl; @@ -20,12 +27,12 @@ public HttpCache(Cache cache, TimeSpan? defaultTtl = null) public void Add(string key, T item) { - _cache.Add(key, item, null, CalculateExpire(_defaultTtl), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); + Add(key,item,_defaultTtl); } public void Add(string key, T item, TimeSpan? timeToLive) { - _cache.Add(key, item, null, CalculateExpire(timeToLive), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); + _cache.Add(key, new CacheItemWrapper(item), null, CalculateExpire(timeToLive), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); } public T Get(string key, Func dataRetriever) where T : class @@ -35,13 +42,14 @@ public T Get(string key, Func dataRetriever) where T : class public T Get(string key, Func dataRetriever, TimeSpan? timeToLive) where T : class { - var item = _cache.Get(key) as T; - if (item != null) - return item; - { - item = dataRetriever.Invoke(); - Add(key, item, timeToLive); - } + var wrapper = _cache.Get(key) as CacheItemWrapper; + if (wrapper != null) + return wrapper.Item as T; + + var item = dataRetriever.Invoke(); + + Add(key, item, timeToLive); + return item; } @@ -52,13 +60,15 @@ public object Get(string key, Type type, Func dataRetriever) public object Get(string key, Type type, Func dataRetriever, TimeSpan? timeToLive) { - var item = _cache.Get(key); - if (item != null && item.GetType() == type) - return item; + var wrapper = _cache.Get(key) as CacheItemWrapper; + if (wrapper != null) + return wrapper.Item; + + var item = dataRetriever.Invoke(); - item = dataRetriever.Invoke(); Add(key, item, timeToLive); - return item.GetType() == type ? item : null; + + return item; } public Task GetAsync(string key, Func> dataRetriever) where T : class diff --git a/source/DoubleCache/DoubleCache.csproj b/source/DoubleCache/DoubleCache.csproj index fc6c56d..11abd89 100644 --- a/source/DoubleCache/DoubleCache.csproj +++ b/source/DoubleCache/DoubleCache.csproj @@ -56,6 +56,7 @@ + diff --git a/source/DoubleCache/LocalCache/MemCache.cs b/source/DoubleCache/LocalCache/MemCache.cs index 731acc4..17bf11e 100644 --- a/source/DoubleCache/LocalCache/MemCache.cs +++ b/source/DoubleCache/LocalCache/MemCache.cs @@ -4,6 +4,7 @@ namespace DoubleCache.LocalCache { + [Obsolete("This implementation does not accept caching of null values, consider using WrappingMemoryCache instead")] public class MemCache : ICacheAside { private readonly TimeSpan? _defaultTtl; diff --git a/source/DoubleCacheTests/DoubleCacheTests.csproj b/source/DoubleCacheTests/DoubleCacheTests.csproj index 07f9f37..3c6ff1d 100644 --- a/source/DoubleCacheTests/DoubleCacheTests.csproj +++ b/source/DoubleCacheTests/DoubleCacheTests.csproj @@ -77,6 +77,7 @@ + diff --git a/source/DoubleCacheTests/IntegrationTests/HttpCacheIntegrationTests.cs b/source/DoubleCacheTests/IntegrationTests/HttpCacheIntegrationTests.cs index ae91650..a15e98b 100644 --- a/source/DoubleCacheTests/IntegrationTests/HttpCacheIntegrationTests.cs +++ b/source/DoubleCacheTests/IntegrationTests/HttpCacheIntegrationTests.cs @@ -1,7 +1,8 @@ using System; using System.IO; using System.Web; -using DoubleCache.SystemWebHttpCache; +using DoubleCache.SystemWebCaching; +using Shouldly; using Xunit; namespace DoubleCacheTests.IntegrationTests @@ -17,5 +18,17 @@ public HttpCacheIntegrationTests() _cacheImplementation = new HttpCache(context.Cache, TimeSpan.FromMinutes(1)); } + + [Fact] + public void Cache_Null_Returns_Null() + { + var key = Guid.NewGuid().ToString(); + + _cacheImplementation.Add(key, null); + + var result = _cacheImplementation.Get(key, () => "a"); + + result.ShouldBeNull(); + } } } From 8d982b1404538e570a21578a6890ff272e0baf8c Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Tue, 25 Oct 2016 10:17:22 +0200 Subject: [PATCH 02/14] Adjusted default factory to use Wrapping memory cache for null support --- source/DoubleCache/CacheFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/DoubleCache/CacheFactory.cs b/source/DoubleCache/CacheFactory.cs index 53999d8..b6ba1d1 100644 --- a/source/DoubleCache/CacheFactory.cs +++ b/source/DoubleCache/CacheFactory.cs @@ -11,7 +11,7 @@ public static ICacheAside CreatePubSubDoubleCache(IConnectionMultiplexer redisCo { var remoteCache = new RedisCache(redisConnection.GetDatabase(), itemSerializer, defaultTtl); return new DoubleCache( - new SubscribingCache(new LocalCache.MemCache(defaultTtl), new RedisSubscriber(redisConnection, remoteCache, itemSerializer)), + new SubscribingCache(new LocalCache.WrappingMemoryCache(defaultTtl), new RedisSubscriber(redisConnection, remoteCache, itemSerializer)), new PublishingCache(remoteCache, new RedisPublisher(redisConnection, itemSerializer))); } } From 7474c6703da6d0fa581092d37079819ecc42c25e Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Tue, 25 Oct 2016 10:21:43 +0200 Subject: [PATCH 03/14] added missing files --- .../LocalCache/WrappingMemoryCache.cs | 112 ++++++++++++++++++ .../WrappingMemoryCacheTests.cs | 29 +++++ 2 files changed, 141 insertions(+) create mode 100644 source/DoubleCache/LocalCache/WrappingMemoryCache.cs create mode 100644 source/DoubleCacheTests/IntegrationTests/WrappingMemoryCacheTests.cs diff --git a/source/DoubleCache/LocalCache/WrappingMemoryCache.cs b/source/DoubleCache/LocalCache/WrappingMemoryCache.cs new file mode 100644 index 0000000..31c5167 --- /dev/null +++ b/source/DoubleCache/LocalCache/WrappingMemoryCache.cs @@ -0,0 +1,112 @@ +using System; +using System.Runtime.Caching; +using System.Threading.Tasks; + +namespace DoubleCache.LocalCache +{ + public class WrappingMemoryCache : ICacheAside + { + internal class CacheItemWrapper + { + internal object Item { get; } + + internal CacheItemWrapper(object item) + { + Item = item; + } + } + + private readonly TimeSpan? _defaultTtl; + + public WrappingMemoryCache(TimeSpan? defaultTtl = null) + { + _defaultTtl = defaultTtl; + } + public void Add(string key, T item) + { + Add(key,item,_defaultTtl); + } + + public void Add(string key, T item, TimeSpan? timeToLive) + { + var policy = new CacheItemPolicy(); + + if (timeToLive.HasValue) + policy.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(timeToLive.Value); + MemoryCache.Default.Set(key, new CacheItemWrapper(item), policy); + } + + public T Get(string key, Func dataRetriever) where T : class + { + return Get(key, dataRetriever, _defaultTtl); + } + + public T Get(string key, Func dataRetriever, TimeSpan? timeToLive) where T : class + { + var wrapper = MemoryCache.Default.Get(key) as CacheItemWrapper; + if (wrapper != null) + return wrapper.Item as T; + + var item = dataRetriever.Invoke(); + Add(key, item, timeToLive); + + return item; + } + + public object Get(string key, Type type, Func dataRetriever) + { + return Get(key, type, dataRetriever, _defaultTtl); + } + + public object Get(string key, Type type, Func dataRetriever, TimeSpan? timeToLive) + { + var wrapper = MemoryCache.Default.Get(key) as CacheItemWrapper; + if (wrapper != null) + return wrapper.Item; + + var item = dataRetriever.Invoke(); + Add(key, item, timeToLive); + return item.GetType() == type ? item : null; + } + + public Task GetAsync(string key, Type type, Func> dataRetriever) + { + return GetAsync(key, type, dataRetriever, _defaultTtl); + } + + public async Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) + { + var wrapper = MemoryCache.Default.Get(key) as CacheItemWrapper; + if (wrapper != null) + return wrapper.Item; + + var item = await dataRetriever.Invoke(); + Add(key, item, timeToLive); + return item.GetType() == type ? item : null; + } + + public Task GetAsync(string key, Func> dataRetriever) where T : class + { + return GetAsync(key, dataRetriever, _defaultTtl); + } + + public async Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class + { + var wrapper = MemoryCache.Default.Get(key) as CacheItemWrapper; + if (wrapper != null) + return wrapper.Item as T; + + var item = await dataRetriever.Invoke(); + Add(key, item, timeToLive); + + return item; + } + + public void Remove(string key) + { + MemoryCache.Default.Remove(key); + } + + public TimeSpan? DefaultTtl { get { return _defaultTtl; } } + } +} diff --git a/source/DoubleCacheTests/IntegrationTests/WrappingMemoryCacheTests.cs b/source/DoubleCacheTests/IntegrationTests/WrappingMemoryCacheTests.cs new file mode 100644 index 0000000..1e3430a --- /dev/null +++ b/source/DoubleCacheTests/IntegrationTests/WrappingMemoryCacheTests.cs @@ -0,0 +1,29 @@ +using System; +using DoubleCache.LocalCache; +using Shouldly; +using Xunit; + +namespace DoubleCacheTests.IntegrationTests +{ + [Trait("Category", "Integration")] + public class WrappingMemoryCacheTests : CacheImplementationTests + { + public WrappingMemoryCacheTests() + { + _key = Guid.NewGuid().ToString(); + _cacheImplementation = new WrappingMemoryCache(TimeSpan.FromMinutes(1)); + } + + [Fact] + public void Cache_Null_Returns_Null() + { + var key = Guid.NewGuid().ToString(); + + _cacheImplementation.Add(key,null); + + var result = _cacheImplementation.Get(key,() => "a" ); + + result.ShouldBeNull(); + } + } +} From c4704e3a0a444750bf00c2fdb1cb5934c762c2f1 Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Tue, 25 Oct 2016 10:31:19 +0200 Subject: [PATCH 04/14] Correctly unwraps item for http cache --- build.fsx | 2 -- .../DoubleCache.SystemWebCaching/HttpCache.cs | 29 +++++++++++-------- source/DoubleCache/Properties/AssemblyInfo.cs | 16 +++++----- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/build.fsx b/build.fsx index 0cf1721..435f830 100644 --- a/build.fsx +++ b/build.fsx @@ -66,7 +66,6 @@ Target "CreateNuget" (fun _ -> WorkingDir = buildDir Version = version Publish = false - Prerelease = true Dependencies = [ "StackExchange.Redis", "1.0.488" "MsgPack.Cli", "0.6.5" @@ -86,7 +85,6 @@ Target "CreateNuget" (fun _ -> WorkingDir = buildDir Version = version Publish = false - Prerelease = true Dependencies = [ "DoubleCache", version ] diff --git a/source/DoubleCache.SystemWebCaching/HttpCache.cs b/source/DoubleCache.SystemWebCaching/HttpCache.cs index 3e6f43e..5827228 100644 --- a/source/DoubleCache.SystemWebCaching/HttpCache.cs +++ b/source/DoubleCache.SystemWebCaching/HttpCache.cs @@ -78,13 +78,14 @@ public Task GetAsync(string key, Func> dataRetriever) where T : cl public async Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class { - var item = _cache.Get(key) as T; - if (item != null) - return item; - { - item = await dataRetriever.Invoke(); - Add(key, item, timeToLive); - } + var wrapper = _cache.Get(key) as CacheItemWrapper; + if (wrapper != null) + return wrapper.Item as T; + + var item = await dataRetriever.Invoke(); + + Add(key, item, timeToLive); + return item; } @@ -95,13 +96,17 @@ public Task GetAsync(string key, Type type, Func> dataRetri public async Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) { - var item = _cache.Get(key); - if (item != null && item.GetType() == type) - return item; + var wrapper = _cache.Get(key) as CacheItemWrapper; + if (wrapper != null) + return wrapper.Item; + + + var item = await dataRetriever.Invoke(); + item = item.GetType() == type ? item : null; - item = await dataRetriever.Invoke(); Add(key, item, timeToLive); - return item.GetType() == type ? item : null; + + return item; } public void Remove(string key) diff --git a/source/DoubleCache/Properties/AssemblyInfo.cs b/source/DoubleCache/Properties/AssemblyInfo.cs index 113fc4f..46e1dd0 100644 --- a/source/DoubleCache/Properties/AssemblyInfo.cs +++ b/source/DoubleCache/Properties/AssemblyInfo.cs @@ -6,19 +6,19 @@ [assembly: AssemblyDescriptionAttribute("Layered distributed cache-aside implementation")] [assembly: GuidAttribute("505f87a8-3062-4070-af1f-cd7358ccd06a")] [assembly: AssemblyProductAttribute("DoubleCache")] -[assembly: AssemblyVersionAttribute("1.6.0")] -[assembly: AssemblyInformationalVersionAttribute("1.6.0")] -[assembly: AssemblyFileVersionAttribute("1.6.0")] -[assembly: AssemblyMetadataAttribute("githash","ac30f57cc769194a07f4112cf91eddc153a50b4a")] +[assembly: AssemblyVersionAttribute("2.0.0")] +[assembly: AssemblyInformationalVersionAttribute("2.0.0-beta.1")] +[assembly: AssemblyFileVersionAttribute("2.0.0")] +[assembly: AssemblyMetadataAttribute("githash","6dd11eaf4f0204d3834644bf1ec8a0736cf80f3d")] namespace System { internal static class AssemblyVersionInformation { internal const System.String AssemblyTitle = "DoubleCache"; internal const System.String AssemblyDescription = "Layered distributed cache-aside implementation"; internal const System.String Guid = "505f87a8-3062-4070-af1f-cd7358ccd06a"; internal const System.String AssemblyProduct = "DoubleCache"; - internal const System.String AssemblyVersion = "1.6.0"; - internal const System.String AssemblyInformationalVersion = "1.6.0"; - internal const System.String AssemblyFileVersion = "1.6.0"; - internal const System.String AssemblyMetadata_githash = "ac30f57cc769194a07f4112cf91eddc153a50b4a"; + internal const System.String AssemblyVersion = "2.0.0"; + internal const System.String AssemblyInformationalVersion = "2.0.0-beta.6"; + internal const System.String AssemblyFileVersion = "2.0.0"; + internal const System.String AssemblyMetadata_githash = "6dd11eaf4f0204d3834644bf1ec8a0736cf80f3d"; } } From 9a0793ca2716e417bffe8a952933f7bb3fd9b73c Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Tue, 25 Oct 2016 10:51:00 +0200 Subject: [PATCH 05/14] Updated readme and build --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index baadeee..896b55a 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,19 @@ public interface ICacheAside The Add and remove methods are implemented with fire and forget, hence it does not need to be Async as this is handled by the StackExchange.Redis client. DoubleCache comes with the following implementations of this interface -* LocalCache.MemCache - using System.Runtime.Memory +* *[obsolete]* LocalCache.MemCache - using System.Runtime.Memory, does not support null items. +* LocalCache.WrappingMemoryCache - Allows storage of null items in Memory cache.* +* SystemWebCaching.HttpCache - An in memory cache using HttpContext.Cache * Redis.RedisCache - using StackExchange.Redis client +* Redis.RedisStaleCache - Use redis as a stale cache in order to mitigate cache stampede. * SubscribingCache - a decorator supporting push notifications of cache updates * PublishingCache - a decorator publishing cache changes * DoubleCache - a decorator wrapping a local and a remote cache +\* using a custom proxy object holding the cache items. This is transparent to the client. + Depending on your cache need, you can combine these implementations and decorators as you need. The most complete example would be a DoubleCache which takes a local cache decorated with a SubscribingCache and a RedisCache decorated with a PublishingCache. This will result in a local cache that will be in sync with the other local caches; if a value isn't found the value will be retrieved from Redis before ultimately being resolved using the func provided to the cache. + +###Redis Stale Cache +Extends TTL for all objects stored in Redis. Verifies TTL when reading data from redis. If this is less than the default TTL on the redis cache, the cached item will be returned and a new fetch from the dataretriever will be extecuted on a separate thread. Storing the result in the cache. + From d61ddbdb796144cf38b2253fa61f68b114edd7b5 Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Wed, 26 Oct 2016 14:17:57 +0200 Subject: [PATCH 06/14] ConfigureAwaitFalse --- source/DoubleCache/LocalCache/MemCache.cs | 4 +- .../LocalCache/WrappingMemoryCache.cs | 6 +- source/DoubleCache/Redis/RedisCache.cs | 10 +- source/DoubleCache/Redis/RedisStaleCache.cs | 16 +- source/DoubleCache/SubscribingCache.cs | 150 +++++++++--------- source/DoubleCacheTests/DoubleCacheTests.cs | 2 +- 6 files changed, 94 insertions(+), 94 deletions(-) diff --git a/source/DoubleCache/LocalCache/MemCache.cs b/source/DoubleCache/LocalCache/MemCache.cs index 17bf11e..12d7b66 100644 --- a/source/DoubleCache/LocalCache/MemCache.cs +++ b/source/DoubleCache/LocalCache/MemCache.cs @@ -75,7 +75,7 @@ public async Task GetAsync(string key, Type type, Func> dat if (item != null && item.GetType() == type) return item; - item = await dataRetriever.Invoke(); + item = await dataRetriever.Invoke().ConfigureAwait(false); Add(key, item, timeToLive); return item.GetType() == type ? item : null; } @@ -91,7 +91,7 @@ public async Task GetAsync(string key, Func> dataRetriever, TimeSp if (item != null) return item; { - item = await dataRetriever.Invoke(); + item = await dataRetriever.Invoke().ConfigureAwait(false); Add(key, item, timeToLive); } return item; diff --git a/source/DoubleCache/LocalCache/WrappingMemoryCache.cs b/source/DoubleCache/LocalCache/WrappingMemoryCache.cs index 31c5167..448998a 100644 --- a/source/DoubleCache/LocalCache/WrappingMemoryCache.cs +++ b/source/DoubleCache/LocalCache/WrappingMemoryCache.cs @@ -80,7 +80,7 @@ public async Task GetAsync(string key, Type type, Func> dat if (wrapper != null) return wrapper.Item; - var item = await dataRetriever.Invoke(); + var item = await dataRetriever.Invoke().ConfigureAwait(false); Add(key, item, timeToLive); return item.GetType() == type ? item : null; } @@ -96,8 +96,8 @@ public async Task GetAsync(string key, Func> dataRetriever, TimeSp if (wrapper != null) return wrapper.Item as T; - var item = await dataRetriever.Invoke(); - Add(key, item, timeToLive); + var item = await dataRetriever.Invoke().ConfigureAwait(false); + Add(key, item, timeToLive); return item; } diff --git a/source/DoubleCache/Redis/RedisCache.cs b/source/DoubleCache/Redis/RedisCache.cs index 2064cc9..64459f6 100644 --- a/source/DoubleCache/Redis/RedisCache.cs +++ b/source/DoubleCache/Redis/RedisCache.cs @@ -82,11 +82,11 @@ public Task GetAsync(string key, Type type, Func> dataRetri public async Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) { - var packedBytes = await _database.StringGetAsync(key); + var packedBytes = await _database.StringGetAsync(key).ConfigureAwait(false); if (!packedBytes.IsNull) return _itemSerializer.Deserialize(packedBytes, type); - - var item = await dataRetriever.Invoke(); + + var item = await dataRetriever.Invoke().ConfigureAwait(false); if (item != null && item.GetType() == type) { Add(key, item, timeToLive); @@ -103,11 +103,11 @@ public Task GetAsync(string key, Func> dataRetriever) where T : cl public async Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class { - var packedBytes = await _database.StringGetAsync(key); + var packedBytes = await _database.StringGetAsync(key).ConfigureAwait(false); if (!packedBytes.IsNull) return _itemSerializer.Deserialize(packedBytes); - var item = await dataRetriever.Invoke(); + var item = await dataRetriever.Invoke().ConfigureAwait(false); Add(key, item, timeToLive); return item; } diff --git a/source/DoubleCache/Redis/RedisStaleCache.cs b/source/DoubleCache/Redis/RedisStaleCache.cs index 955fc5e..ce33a2e 100644 --- a/source/DoubleCache/Redis/RedisStaleCache.cs +++ b/source/DoubleCache/Redis/RedisStaleCache.cs @@ -119,21 +119,21 @@ public async Task GetAsync(string key, Func> dataRetriever, TimeSp if (timeToLive.HasValue) staleTtl = timeToLive.Value.Add(_staleDuration); - var item = await _redisCache.GetAsync(key, dataRetriever, staleTtl); - var ttl = await _database.KeyTimeToLiveAsync(key); + var item = await _redisCache.GetAsync(key, dataRetriever, staleTtl).ConfigureAwait(false); + var ttl = await _database.KeyTimeToLiveAsync(key).ConfigureAwait(false); if (!ttl.HasValue || ttl.Value < _staleDuration) { ttl = ttl == null ? _staleDuration.Add(_staleDuration) : ttl.Value.Add(_staleDuration); - await _database.KeyExpireAsync(key, ttl); + await _database.KeyExpireAsync(key, ttl).ConfigureAwait(false); ThreadPool.QueueUserWorkItem(async o => { try { - _redisCache.Add(key, await dataRetriever.Invoke(), staleTtl); + _redisCache.Add(key, await dataRetriever.Invoke().ConfigureAwait(false), staleTtl); } catch { //make sure we do not crash. @@ -154,21 +154,21 @@ public async Task GetAsync(string key, Type type, Func> dat if (timeToLive.HasValue) staleTtl = timeToLive.Value.Add(_staleDuration); - var item = await _redisCache.GetAsync(key, type, dataRetriever, staleTtl); - var ttl = await _database.KeyTimeToLiveAsync(key); + var item = await _redisCache.GetAsync(key, type, dataRetriever, staleTtl).ConfigureAwait(false); + var ttl = await _database.KeyTimeToLiveAsync(key).ConfigureAwait(false); if (!ttl.HasValue || ttl.Value < _staleDuration) { ttl = ttl == null ? _staleDuration.Add(_staleDuration) : ttl.Value.Add(_staleDuration); - await _database.KeyExpireAsync(key, ttl); + await _database.KeyExpireAsync(key, ttl).ConfigureAwait(false); ThreadPool.QueueUserWorkItem(async o => { try { - _redisCache.Add(key, await dataRetriever.Invoke(), timeToLive); + _redisCache.Add(key, await dataRetriever.Invoke().ConfigureAwait(false), timeToLive); } catch { //make sure we do not crash. diff --git a/source/DoubleCache/SubscribingCache.cs b/source/DoubleCache/SubscribingCache.cs index e155559..0427d61 100644 --- a/source/DoubleCache/SubscribingCache.cs +++ b/source/DoubleCache/SubscribingCache.cs @@ -1,33 +1,33 @@ -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; - -namespace DoubleCache -{ - public class SubscribingCache : ICacheAside - { - private readonly ICacheAside _cache; - private readonly ICacheSubscriber _cacheSubscriber; - private readonly ConcurrentDictionary _knownTypes; - - public SubscribingCache(ICacheAside cache, ICacheSubscriber cacheSubscriber) - { - _knownTypes = new ConcurrentDictionary(); - _cache = cache; - _cacheSubscriber = cacheSubscriber; - - _cacheSubscriber.CacheUpdate += OnCacheUpdate; - _cacheSubscriber.CacheDelete += OnCacheDelete; - } - - public void Add(string key, T item) - { - _cache.Add(key, item); - } - - public void Add(string key, T item, TimeSpan? timeToLive) - { - _cache.Add(key, item, timeToLive); +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace DoubleCache +{ + public class SubscribingCache : ICacheAside + { + private readonly ICacheAside _cache; + private readonly ICacheSubscriber _cacheSubscriber; + private readonly ConcurrentDictionary _knownTypes; + + public SubscribingCache(ICacheAside cache, ICacheSubscriber cacheSubscriber) + { + _knownTypes = new ConcurrentDictionary(); + _cache = cache; + _cacheSubscriber = cacheSubscriber; + + _cacheSubscriber.CacheUpdate += OnCacheUpdate; + _cacheSubscriber.CacheDelete += OnCacheDelete; + } + + public void Add(string key, T item) + { + _cache.Add(key, item); + } + + public void Add(string key, T item, TimeSpan? timeToLive) + { + _cache.Add(key, item, timeToLive); } public T Get(string key, Func dataRetriever) where T : class @@ -48,43 +48,43 @@ public object Get(string key, Type type, Func dataRetriever) public object Get(string key, Type type, Func dataRetriever, TimeSpan? timeToLive) { return _cache.Get(key, type, dataRetriever, timeToLive); - } - - public Task GetAsync(string key, Type type, Func> dataRetriever) - { - return _cache.GetAsync(key, type, dataRetriever); - } - - public Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) - { - return _cache.GetAsync(key, type, dataRetriever, timeToLive); - } - - public Task GetAsync(string key, Func> dataRetriever) where T : class - { - return _cache.GetAsync(key, dataRetriever); - } - - public Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class - { - return _cache.GetAsync(key, dataRetriever, timeToLive); - } - - private async void OnCacheUpdate(object sender, CacheUpdateNotificationArgs e) - { - await CacheUpdateAction(sender, e); - } - private async Task CacheUpdateAction(object sender, CacheUpdateNotificationArgs e) - { - var remoteItem = await _cacheSubscriber.GetAsync(e.Key, _knownTypes.GetOrAdd(e.Type, Type.GetType(e.Type))); - - if (remoteItem != null) - { - if (e.SpecificTimeToLive != null) - Add(e.Key, remoteItem, e.SpecificTimeToLive._timeToLive); - else - Add(e.Key, remoteItem); - } + } + + public Task GetAsync(string key, Type type, Func> dataRetriever) + { + return _cache.GetAsync(key, type, dataRetriever); + } + + public Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) + { + return _cache.GetAsync(key, type, dataRetriever, timeToLive); + } + + public Task GetAsync(string key, Func> dataRetriever) where T : class + { + return _cache.GetAsync(key, dataRetriever); + } + + public Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class + { + return _cache.GetAsync(key, dataRetriever, timeToLive); + } + + private async void OnCacheUpdate(object sender, CacheUpdateNotificationArgs e) + { + await CacheUpdateAction(sender, e).ConfigureAwait(false); + } + private async Task CacheUpdateAction(object sender, CacheUpdateNotificationArgs e) + { + var remoteItem = await _cacheSubscriber.GetAsync(e.Key, _knownTypes.GetOrAdd(e.Type, Type.GetType(e.Type))).ConfigureAwait(false); ; + + if (remoteItem != null) + { + if (e.SpecificTimeToLive != null) + Add(e.Key, remoteItem, e.SpecificTimeToLive._timeToLive); + else + Add(e.Key, remoteItem); + } } private void OnCacheDelete(object sender, CacheUpdateNotificationArgs e) @@ -92,11 +92,11 @@ private void OnCacheDelete(object sender, CacheUpdateNotificationArgs e) Remove(e.Key); } - public void Remove(string key) - { - _cache.Remove(key); - } - - public TimeSpan? DefaultTtl { get { return _cache.DefaultTtl; } } - } -} + public void Remove(string key) + { + _cache.Remove(key); + } + + public TimeSpan? DefaultTtl { get { return _cache.DefaultTtl; } } + } +} diff --git a/source/DoubleCacheTests/DoubleCacheTests.cs b/source/DoubleCacheTests/DoubleCacheTests.cs index 2740b76..c55d33c 100644 --- a/source/DoubleCacheTests/DoubleCacheTests.cs +++ b/source/DoubleCacheTests/DoubleCacheTests.cs @@ -81,7 +81,7 @@ public void GetGeneric_WithTimeToLive_CalledOnLocal() [Fact] public async Task GetAsync_CalledOnLocal() { - await _doubleCache.GetAsync("A", typeof(string), null); + await _doubleCache.GetAsync("A", typeof(string), null).ConfigureAwait(false); A.CallTo(() => _local.GetAsync("A", A.Ignored, A>>.Ignored)).MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _remote.GetAsync("A", A.Ignored, A>>.Ignored)).MustNotHaveHappened(); From dec9f54eb2a4442298e2681b6ba85c674addc4cc Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Thu, 27 Oct 2016 10:24:24 +0200 Subject: [PATCH 07/14] Null caching verification --- source/DoubleCache/DoubleCache.cs | 122 +++++++++--------- source/DoubleCache/LocalCache/MemCache.cs | 2 +- .../LocalCache/WrappingMemoryCache.cs | 2 +- .../BinaryFormatterItemSerializer.cs | 21 +++ .../DoubleCacheTests/DoubleCacheTests.csproj | 1 + .../CacheImplementationTests.cs | 13 +- .../HttpCacheIntegrationTests.cs | 12 -- .../IntegrationTests/MemoryCacheTests.cs | 7 + .../IntegrationTests/PubSubDoubleCacheTest.cs | 19 +++ .../WrappingMemoryCacheTests.cs | 12 -- .../Serialization/ItemSerializerTests.cs | 69 +++++----- 11 files changed, 160 insertions(+), 120 deletions(-) create mode 100644 source/DoubleCacheTests/IntegrationTests/PubSubDoubleCacheTest.cs diff --git a/source/DoubleCache/DoubleCache.cs b/source/DoubleCache/DoubleCache.cs index 60e85ac..af77f7f 100644 --- a/source/DoubleCache/DoubleCache.cs +++ b/source/DoubleCache/DoubleCache.cs @@ -1,29 +1,29 @@ -using System; -using System.Threading.Tasks; - -namespace DoubleCache -{ - public class DoubleCache : ICacheAside - { - private readonly ICacheAside _localCache; - private readonly ICacheAside _remoteCache; - - public DoubleCache(ICacheAside localCache,ICacheAside remoteCache) - { - _localCache = localCache; - _remoteCache = remoteCache; - } - - public void Add(string key, T item) - { - _localCache.Add(key, item); - _remoteCache.Add(key, item); - } - - public void Add(string key, T item, TimeSpan? timeToLive) - { - _localCache.Add(key, item, timeToLive); - _remoteCache.Add(key, item, timeToLive); +using System; +using System.Threading.Tasks; + +namespace DoubleCache +{ + public class DoubleCache : ICacheAside + { + private readonly ICacheAside _localCache; + private readonly ICacheAside _remoteCache; + + public DoubleCache(ICacheAside localCache,ICacheAside remoteCache) + { + _localCache = localCache; + _remoteCache = remoteCache; + } + + public void Add(string key, T item) + { + _localCache.Add(key, item); + _remoteCache.Add(key, item); + } + + public void Add(string key, T item, TimeSpan? timeToLive) + { + _localCache.Add(key, item, timeToLive); + _remoteCache.Add(key, item, timeToLive); } public T Get(string key, Func dataRetriever) where T : class @@ -45,41 +45,41 @@ public object Get(string key, Type type, Func dataRetriever) public object Get(string key, Type type, Func dataRetriever, TimeSpan? timeToLive) { return _localCache.Get(key, type, () => _remoteCache.Get(key, type, dataRetriever, timeToLive), timeToLive); - } - - public Task GetAsync(string key, Type type, Func> dataRetriever) - { - return _localCache.GetAsync(key, type, () => _remoteCache.GetAsync(key, type, dataRetriever)); - } - - public Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) - { - return _localCache.GetAsync(key, type, () => _remoteCache.GetAsync(key, type, dataRetriever), timeToLive); - } - - public Task GetAsync(string key, Func> dataRetriever) where T : class - { - return _localCache.GetAsync(key, () => _remoteCache.GetAsync(key, dataRetriever)); - } - - public Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class - { - return _localCache.GetAsync(key, () => _remoteCache.GetAsync(key, dataRetriever),timeToLive); } - public void Remove(string key) - { - _localCache.Remove(key); - _remoteCache.Remove(key); - } - - public TimeSpan? DefaultTtl { get - { - return _localCache.DefaultTtl > _remoteCache.DefaultTtl - ? _localCache.DefaultTtl - : _remoteCache.DefaultTtl; - } } - + public Task GetAsync(string key, Type type, Func> dataRetriever) + { + return _localCache.GetAsync(key, type, () => _remoteCache.GetAsync(key, type, dataRetriever)); + } + + public Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) + { + return _localCache.GetAsync(key, type, () => _remoteCache.GetAsync(key, type, dataRetriever), timeToLive); + } + + public Task GetAsync(string key, Func> dataRetriever) where T : class + { + return _localCache.GetAsync(key, () => _remoteCache.GetAsync(key, dataRetriever)); + } + + public Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class + { + return _localCache.GetAsync(key, () => _remoteCache.GetAsync(key, dataRetriever),timeToLive); + } + + public void Remove(string key) + { + _localCache.Remove(key); + _remoteCache.Remove(key); + } + + public TimeSpan? DefaultTtl { get + { + return _localCache.DefaultTtl > _remoteCache.DefaultTtl + ? _localCache.DefaultTtl + : _remoteCache.DefaultTtl; + } } + - } -} + } +} diff --git a/source/DoubleCache/LocalCache/MemCache.cs b/source/DoubleCache/LocalCache/MemCache.cs index 12d7b66..044b5d7 100644 --- a/source/DoubleCache/LocalCache/MemCache.cs +++ b/source/DoubleCache/LocalCache/MemCache.cs @@ -8,7 +8,7 @@ namespace DoubleCache.LocalCache public class MemCache : ICacheAside { private readonly TimeSpan? _defaultTtl; - + public MemCache(TimeSpan? defaultTtl = null) { _defaultTtl = defaultTtl; diff --git a/source/DoubleCache/LocalCache/WrappingMemoryCache.cs b/source/DoubleCache/LocalCache/WrappingMemoryCache.cs index 448998a..8b0e1d2 100644 --- a/source/DoubleCache/LocalCache/WrappingMemoryCache.cs +++ b/source/DoubleCache/LocalCache/WrappingMemoryCache.cs @@ -82,7 +82,7 @@ public async Task GetAsync(string key, Type type, Func> dat var item = await dataRetriever.Invoke().ConfigureAwait(false); Add(key, item, timeToLive); - return item.GetType() == type ? item : null; + return item == null || item.GetType() == type ? item : null; } public Task GetAsync(string key, Func> dataRetriever) where T : class diff --git a/source/DoubleCache/Serialization/BinaryFormatterItemSerializer.cs b/source/DoubleCache/Serialization/BinaryFormatterItemSerializer.cs index 77b54bc..a90a55a 100644 --- a/source/DoubleCache/Serialization/BinaryFormatterItemSerializer.cs +++ b/source/DoubleCache/Serialization/BinaryFormatterItemSerializer.cs @@ -1,13 +1,21 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Runtime.Serialization.Formatters.Binary; namespace DoubleCache.Serialization { public class BinaryFormatterItemSerializer : IItemSerializer { + private static ConcurrentDictionary _typeCache = new ConcurrentDictionary(); + public byte[] Serialize(T item) { + if (item == null) + return new byte[0]; + var formatter = new BinaryFormatter(); byte[] itemBytes; @@ -23,6 +31,9 @@ public byte[] Serialize(T item) public object Deserialize(byte[] bytes, Type type) { + if (bytes.Length == 0) + return type.IsValueType ? Activator.CreateInstance(type) : null; + var formatter = new BinaryFormatter(); object item; @@ -36,6 +47,9 @@ public object Deserialize(byte[] bytes, Type type) public T Deserialize(Stream stream) { + if (stream.Length == 0) + return default(T); + var formatter = new BinaryFormatter(); return (T)formatter.Deserialize(stream); @@ -44,6 +58,9 @@ public T Deserialize(Stream stream) public T Deserialize(byte[] bytes) { + if (bytes.Length == 0) + return default(T); + var formatter = new BinaryFormatter(); object item; @@ -54,6 +71,10 @@ public T Deserialize(byte[] bytes) return (T)item; } + private static T GetDefault() + { + return default(T); + } } } diff --git a/source/DoubleCacheTests/DoubleCacheTests.csproj b/source/DoubleCacheTests/DoubleCacheTests.csproj index 3c6ff1d..6e12908 100644 --- a/source/DoubleCacheTests/DoubleCacheTests.csproj +++ b/source/DoubleCacheTests/DoubleCacheTests.csproj @@ -76,6 +76,7 @@ + diff --git a/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs b/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs index de14215..67186e7 100644 --- a/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs +++ b/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs @@ -12,7 +12,6 @@ public abstract class CacheImplementationTests protected string _key; protected ICacheAside _cacheImplementation; - [Fact] public void Get_ExistingValue_ReturnsValue() { @@ -252,5 +251,17 @@ public async Task Remove_ExistingKey_DeletesValue() A.CallTo(() => func.Invoke()).MustHaveHappened(Repeated.Exactly.Once); result.ShouldBe("B"); } + + [Fact] + public virtual void Cache_Null_Returns_Null() + { + var key = Guid.NewGuid().ToString(); + + _cacheImplementation.Add(key, null); + + var result = _cacheImplementation.Get(key, () => "a"); + + result.ShouldBeNull(); + } } } diff --git a/source/DoubleCacheTests/IntegrationTests/HttpCacheIntegrationTests.cs b/source/DoubleCacheTests/IntegrationTests/HttpCacheIntegrationTests.cs index a15e98b..f9b5eeb 100644 --- a/source/DoubleCacheTests/IntegrationTests/HttpCacheIntegrationTests.cs +++ b/source/DoubleCacheTests/IntegrationTests/HttpCacheIntegrationTests.cs @@ -18,17 +18,5 @@ public HttpCacheIntegrationTests() _cacheImplementation = new HttpCache(context.Cache, TimeSpan.FromMinutes(1)); } - - [Fact] - public void Cache_Null_Returns_Null() - { - var key = Guid.NewGuid().ToString(); - - _cacheImplementation.Add(key, null); - - var result = _cacheImplementation.Get(key, () => "a"); - - result.ShouldBeNull(); - } } } diff --git a/source/DoubleCacheTests/IntegrationTests/MemoryCacheTests.cs b/source/DoubleCacheTests/IntegrationTests/MemoryCacheTests.cs index 2d6bb58..d864f66 100644 --- a/source/DoubleCacheTests/IntegrationTests/MemoryCacheTests.cs +++ b/source/DoubleCacheTests/IntegrationTests/MemoryCacheTests.cs @@ -1,5 +1,7 @@ using System; using DoubleCache.LocalCache; +using FakeItEasy; +using Shouldly; using Xunit; namespace DoubleCacheTests.IntegrationTests @@ -12,5 +14,10 @@ public MemoryCacheIntegrationTests() _key = Guid.NewGuid().ToString(); _cacheImplementation = new MemCache(TimeSpan.FromMinutes(1)); } + + public override void Cache_Null_Returns_Null() + { + Should.Throw(() => base.Cache_Null_Returns_Null()); + } } } diff --git a/source/DoubleCacheTests/IntegrationTests/PubSubDoubleCacheTest.cs b/source/DoubleCacheTests/IntegrationTests/PubSubDoubleCacheTest.cs new file mode 100644 index 0000000..454dffa --- /dev/null +++ b/source/DoubleCacheTests/IntegrationTests/PubSubDoubleCacheTest.cs @@ -0,0 +1,19 @@ +using System; +using DoubleCache; +using DoubleCache.Serialization; +using Xunit; + +namespace DoubleCacheTests.IntegrationTests +{ + + [Trait("Category", "Integration")] + public class PubSubDoubleCacheTest : CacheImplementationTests, IClassFixture + { + public PubSubDoubleCacheTest(RedisFixture fixture) + { + _key = Guid.NewGuid().ToString(); + _cacheImplementation = CacheFactory.CreatePubSubDoubleCache(fixture.ConnectionMultiplexer, + new BinaryFormatterItemSerializer(),TimeSpan.FromMinutes(1)); + } + } +} diff --git a/source/DoubleCacheTests/IntegrationTests/WrappingMemoryCacheTests.cs b/source/DoubleCacheTests/IntegrationTests/WrappingMemoryCacheTests.cs index 1e3430a..8c34626 100644 --- a/source/DoubleCacheTests/IntegrationTests/WrappingMemoryCacheTests.cs +++ b/source/DoubleCacheTests/IntegrationTests/WrappingMemoryCacheTests.cs @@ -13,17 +13,5 @@ public WrappingMemoryCacheTests() _key = Guid.NewGuid().ToString(); _cacheImplementation = new WrappingMemoryCache(TimeSpan.FromMinutes(1)); } - - [Fact] - public void Cache_Null_Returns_Null() - { - var key = Guid.NewGuid().ToString(); - - _cacheImplementation.Add(key,null); - - var result = _cacheImplementation.Get(key,() => "a" ); - - result.ShouldBeNull(); - } } } diff --git a/source/DoubleCacheTests/Serialization/ItemSerializerTests.cs b/source/DoubleCacheTests/Serialization/ItemSerializerTests.cs index 35fac8c..c42c5a6 100644 --- a/source/DoubleCacheTests/Serialization/ItemSerializerTests.cs +++ b/source/DoubleCacheTests/Serialization/ItemSerializerTests.cs @@ -7,40 +7,45 @@ namespace DoubleCacheTests.Serialization { - public abstract class ItemSerializerTests - { + public abstract class ItemSerializerTests + { protected IItemSerializer serializer; - [Theory] + [Theory] [InlineData("a")] - public void RoundtripSerializeGeneric(T input) - { - var result = serializer.Deserialize(serializer.Serialize(input)); - - result.ShouldBe(input); - } - [Theory] - [InlineData("a")] - [CacheNotificationData("test","test",1)] - public void RoundtripSerialize(T input) - { - var result = serializer.Deserialize(serializer.Serialize(input), typeof(T)); - - result.ShouldBeOfType(); - - if (result is string) - result.ShouldBe(input); - } - - [Theory] - [InlineData("a")] - public void RoundtripDeserializeStream(T input) - { - using (var ms = new MemoryStream(serializer.Serialize(input))) - { - var result = serializer.Deserialize(ms); - result.ShouldBe(input); - } - } + public void RoundtripSerializeGeneric(T input) + { + var result = serializer.Deserialize(serializer.Serialize(input)); + + result.ShouldBe(input); + } + [Theory] + [InlineData("a")] + [InlineData(null)] + [CacheNotificationData("test","test",1)] + public void RoundtripSerialize(T input) + { + var result = serializer.Deserialize(serializer.Serialize(input), typeof(T)); + + if (input == null) + result.ShouldBeNull(); + else + { + result.ShouldBeOfType(); + if (result is string) + result.ShouldBe(input); + } + } + + [Theory] + [InlineData("a")] + public void RoundtripDeserializeStream(T input) + { + using (var ms = new MemoryStream(serializer.Serialize(input))) + { + var result = serializer.Deserialize(ms); + result.ShouldBe(input); + } + } } } From ff5c348c599f7d861f99fc484e8f5c97b7b73f46 Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Thu, 25 May 2017 13:34:56 +0200 Subject: [PATCH 08/14] Cleanup tests --- source/DoubleCache/Properties/AssemblyInfo.cs | 6 +- source/DoubleCache/PublishingCache.cs | 4 +- .../CacheImplementationTests.cs | 12 + .../IntegrationTests/MemoryCacheTests.cs | 5 + .../DoubleCacheTests/SubscribingCacheTests.cs | 272 +++++++++--------- 5 files changed, 158 insertions(+), 141 deletions(-) diff --git a/source/DoubleCache/Properties/AssemblyInfo.cs b/source/DoubleCache/Properties/AssemblyInfo.cs index 46e1dd0..3011a91 100644 --- a/source/DoubleCache/Properties/AssemblyInfo.cs +++ b/source/DoubleCache/Properties/AssemblyInfo.cs @@ -7,9 +7,9 @@ [assembly: GuidAttribute("505f87a8-3062-4070-af1f-cd7358ccd06a")] [assembly: AssemblyProductAttribute("DoubleCache")] [assembly: AssemblyVersionAttribute("2.0.0")] -[assembly: AssemblyInformationalVersionAttribute("2.0.0-beta.1")] +[assembly: AssemblyInformationalVersionAttribute("2.0.0-beta.6")] [assembly: AssemblyFileVersionAttribute("2.0.0")] -[assembly: AssemblyMetadataAttribute("githash","6dd11eaf4f0204d3834644bf1ec8a0736cf80f3d")] +[assembly: AssemblyMetadataAttribute("githash","dec9f54eb2a4442298e2681b6ba85c674addc4cc")] namespace System { internal static class AssemblyVersionInformation { internal const System.String AssemblyTitle = "DoubleCache"; @@ -19,6 +19,6 @@ internal static class AssemblyVersionInformation { internal const System.String AssemblyVersion = "2.0.0"; internal const System.String AssemblyInformationalVersion = "2.0.0-beta.6"; internal const System.String AssemblyFileVersion = "2.0.0"; - internal const System.String AssemblyMetadata_githash = "6dd11eaf4f0204d3834644bf1ec8a0736cf80f3d"; + internal const System.String AssemblyMetadata_githash = "dec9f54eb2a4442298e2681b6ba85c674addc4cc"; } } diff --git a/source/DoubleCache/PublishingCache.cs b/source/DoubleCache/PublishingCache.cs index 1f94970..2281ec5 100644 --- a/source/DoubleCache/PublishingCache.cs +++ b/source/DoubleCache/PublishingCache.cs @@ -17,13 +17,13 @@ public PublishingCache(ICacheAside cache, ICachePublisher cachePublisher) public void Add(string key, T item) { _cache.Add(key, item); - _cachePublisher.NotifyUpdate(key, item.GetType().AssemblyQualifiedName); + _cachePublisher.NotifyUpdate(key, typeof(T).AssemblyQualifiedName); } public void Add(string key, T item, TimeSpan? timeToLive) { _cache.Add(key, item, timeToLive); - _cachePublisher.NotifyUpdate(key, item.GetType().AssemblyQualifiedName); + _cachePublisher.NotifyUpdate(key, typeof(T).AssemblyQualifiedName, timeToLive); } public T Get(string key, Func dataRetriever) where T : class diff --git a/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs b/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs index 67186e7..9c53b3f 100644 --- a/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs +++ b/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs @@ -263,5 +263,17 @@ public virtual void Cache_Null_Returns_Null() result.ShouldBeNull(); } + + [Fact] + public virtual void CacheWithTTL_Null_Returns_Null() + { + var key = Guid.NewGuid().ToString(); + + _cacheImplementation.Add(key, null, TimeSpan.FromMinutes(1)); + + var result = _cacheImplementation.Get(key, () => "a"); + + result.ShouldBeNull(); + } } } diff --git a/source/DoubleCacheTests/IntegrationTests/MemoryCacheTests.cs b/source/DoubleCacheTests/IntegrationTests/MemoryCacheTests.cs index d864f66..f8dd6fa 100644 --- a/source/DoubleCacheTests/IntegrationTests/MemoryCacheTests.cs +++ b/source/DoubleCacheTests/IntegrationTests/MemoryCacheTests.cs @@ -19,5 +19,10 @@ public override void Cache_Null_Returns_Null() { Should.Throw(() => base.Cache_Null_Returns_Null()); } + + public override void CacheWithTTL_Null_Returns_Null() + { + Should.Throw(() => base.CacheWithTTL_Null_Returns_Null()); + } } } diff --git a/source/DoubleCacheTests/SubscribingCacheTests.cs b/source/DoubleCacheTests/SubscribingCacheTests.cs index 9034957..eafdd7d 100644 --- a/source/DoubleCacheTests/SubscribingCacheTests.cs +++ b/source/DoubleCacheTests/SubscribingCacheTests.cs @@ -1,135 +1,135 @@ -using DoubleCache; -using FakeItEasy; -using System; -using System.Threading.Tasks; -using Xunit; - -namespace DoubleCacheTests -{ - public class SubscribingCacheTests - { - private readonly ICacheAside _decoratedCache; - private readonly ICacheSubscriber _subscriber; - private readonly ICacheAside _subscribingCache; - - public SubscribingCacheTests() - { - _decoratedCache = A.Fake(); - _subscriber = A.Fake(); - _subscribingCache = new SubscribingCache(_decoratedCache, _subscriber); - } - - [Fact] - public void Add_CallsThrough() - { - _subscribingCache.Add("a", "b"); - - A.CallTo(() => _decoratedCache.Add("a", "b")).MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public void Add_WithTtl_CallsThrough() - { - _subscribingCache.Add("a", "b", TimeSpan.FromMinutes(1)); - - A.CallTo(() => _decoratedCache.Add("a", "b", TimeSpan.FromMinutes(1))).MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public void Get_CallsThrough() - { - _subscribingCache.Get("a", typeof(string), null); - A.CallTo(() => _decoratedCache.Get("a", typeof(string), A>._)) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public void Get_WithTimeToLive_CallsThrough() - { - _subscribingCache.Get("a", typeof(string), null, TimeSpan.FromSeconds(1)); - A.CallTo(() => _decoratedCache.Get("a", typeof(string), A>._, TimeSpan.FromSeconds(1))) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public void GetGeneric_CallsThrough() - { - _subscribingCache.Get("a", A.Fake>()); - A.CallTo(() => _decoratedCache.Get("a", A>._)) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public void GetGeneric_WithTimeToLive_CallsThrough() - { - _subscribingCache.Get("a", A.Fake>(), TimeSpan.FromSeconds(1)); - A.CallTo(() => _decoratedCache.Get("a", A>._, TimeSpan.FromSeconds(1))) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public async Task GetAsync_CallsThrough() - { - await _subscribingCache.GetAsync("a", typeof(string), null); - A.CallTo(() => _decoratedCache.GetAsync("a", typeof(string), A>>._)) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public async Task GetAsync_WithTimeToLive_CallsThrough() - { - await _subscribingCache.GetAsync("a", typeof(string), null, TimeSpan.FromSeconds(1)); - A.CallTo(() => _decoratedCache.GetAsync("a", typeof(string), A>>._, TimeSpan.FromSeconds(1))) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public async Task GetAsyncGeneric_CallsThrough() - { - await _subscribingCache.GetAsync("a", A.Fake>>()); - A.CallTo(() => _decoratedCache.GetAsync("a", A>>._)) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public async Task GetAsyncGeneric_WithTimeToLive_CallsThrough() - { - await _subscribingCache.GetAsync("a", A.Fake>>(), TimeSpan.FromSeconds(1)); - A.CallTo(() => _decoratedCache.GetAsync("a", A>>._, TimeSpan.FromSeconds(1))) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public async Task SubscriberUpdate_ItemAdded() - { - A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); - _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName }); - - await Task.Delay(TimeSpan.FromSeconds(1)); - A.CallTo(() => _decoratedCache.Add("a", "b")) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public async Task SubscriberUpdate_WithSpecificTimeToLive_null_ItemAddedWithTimeToLive() - { - A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); - _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName, SpecificTimeToLive = new TimeToLive(null) }); - - await Task.Delay(TimeSpan.FromSeconds(1)); - A.CallTo(() => _decoratedCache.Add("a", "b", null)) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public async Task SubscriberUpdate_WithSpecificTimeToLive_value_ItemAddedWithTimeToLive() - { - A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); - _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName, SpecificTimeToLive = new TimeToLive(TimeSpan.FromMinutes(1)) }); - - await Task.Delay(TimeSpan.FromSeconds(1)); - A.CallTo(() => _decoratedCache.Add("a", "b", TimeSpan.FromMinutes(1))) - .MustHaveHappened(Repeated.Exactly.Once); +using DoubleCache; +using FakeItEasy; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DoubleCacheTests +{ + public class SubscribingCacheTests + { + private readonly ICacheAside _decoratedCache; + private readonly ICacheSubscriber _subscriber; + private readonly ICacheAside _subscribingCache; + + public SubscribingCacheTests() + { + _decoratedCache = A.Fake(); + _subscriber = A.Fake(); + _subscribingCache = new SubscribingCache(_decoratedCache, _subscriber); + } + + [Fact] + public void Add_CallsThrough() + { + _subscribingCache.Add("a", "b"); + + A.CallTo(() => _decoratedCache.Add("a", "b")).MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void Add_WithTtl_CallsThrough() + { + _subscribingCache.Add("a", "b", TimeSpan.FromMinutes(1)); + + A.CallTo(() => _decoratedCache.Add("a", "b", TimeSpan.FromMinutes(1))).MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void Get_CallsThrough() + { + _subscribingCache.Get("a", typeof(string), null); + A.CallTo(() => _decoratedCache.Get("a", typeof(string), A>._)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void Get_WithTimeToLive_CallsThrough() + { + _subscribingCache.Get("a", typeof(string), null, TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Get("a", typeof(string), A>._, TimeSpan.FromSeconds(1))) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void GetGeneric_CallsThrough() + { + _subscribingCache.Get("a", A.Fake>()); + A.CallTo(() => _decoratedCache.Get("a", A>._)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void GetGeneric_WithTimeToLive_CallsThrough() + { + _subscribingCache.Get("a", A.Fake>(), TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Get("a", A>._, TimeSpan.FromSeconds(1))) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task GetAsync_CallsThrough() + { + await _subscribingCache.GetAsync("a", typeof(string), null); + A.CallTo(() => _decoratedCache.GetAsync("a", typeof(string), A>>._)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task GetAsync_WithTimeToLive_CallsThrough() + { + await _subscribingCache.GetAsync("a", typeof(string), null, TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.GetAsync("a", typeof(string), A>>._, TimeSpan.FromSeconds(1))) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task GetAsyncGeneric_CallsThrough() + { + await _subscribingCache.GetAsync("a", A.Fake>>()); + A.CallTo(() => _decoratedCache.GetAsync("a", A>>._)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task GetAsyncGeneric_WithTimeToLive_CallsThrough() + { + await _subscribingCache.GetAsync("a", A.Fake>>(), TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.GetAsync("a", A>>._, TimeSpan.FromSeconds(1))) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task SubscriberUpdate_ItemAdded() + { + A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); + _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName }); + + await Task.Delay(TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Add("a", "b")) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task SubscriberUpdate_WithSpecificTimeToLive_null_ItemAddedWithTimeToLive() + { + A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); + _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName, SpecificTimeToLive = new TimeToLive(null) }); + + await Task.Delay(TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Add("a", "b", null)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task SubscriberUpdate_WithSpecificTimeToLive_value_ItemAddedWithTimeToLive() + { + A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); + _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName, SpecificTimeToLive = new TimeToLive(TimeSpan.FromMinutes(1)) }); + + await Task.Delay(TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Add("a", "b", TimeSpan.FromMinutes(1))) + .MustHaveHappened(Repeated.Exactly.Once); } [Fact] @@ -143,7 +143,7 @@ public async Task SubscriberUpdater_NullReturned_DoesNotAddToCache() .MustNotHaveHappened(); } - [Fact] + [Fact] public async Task SubscriberDelete_ItemDelete() { _subscriber.CacheDelete += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a" }); @@ -151,6 +151,6 @@ public async Task SubscriberDelete_ItemDelete() await Task.Delay(TimeSpan.FromSeconds(1)); A.CallTo(() => _decoratedCache.Remove("a")) .MustHaveHappened(Repeated.Exactly.Once); - } - } -} + } + } +} From 52bfcfd6df28a9d44edb99dde4f60e3c71641b5c Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Thu, 25 May 2017 13:36:49 +0200 Subject: [PATCH 09/14] Update Redis client --- source/DoubleCache/DoubleCache.csproj | 6 +++--- source/DoubleCache/packages.config | 2 +- source/DoubleCacheTests/DoubleCacheTests.csproj | 6 +++--- source/DoubleCacheTests/packages.config | 2 +- source/Sample/RandomUser/RandomUser.csproj | 6 +++--- source/Sample/RandomUser/packages.config | 2 +- .../SampleApiCacheUpdateConsole.csproj | 6 +++--- source/Sample/SampleApiCacheUpdateConsole/packages.config | 2 +- .../Sample/SampleApiHostConsole/SampleApiHostConsole.csproj | 6 +++--- source/Sample/SampleApiHostConsole/packages.config | 2 +- source/Sample/SampleApiOwin/SampleApiOwin.csproj | 6 +++--- source/Sample/SampleApiOwin/packages.config | 2 +- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/source/DoubleCache/DoubleCache.csproj b/source/DoubleCache/DoubleCache.csproj index 11abd89..d806195 100644 --- a/source/DoubleCache/DoubleCache.csproj +++ b/source/DoubleCache/DoubleCache.csproj @@ -34,12 +34,12 @@ ..\packages\MsgPack.Cli.0.6.5\lib\net45\MsgPack.dll True - - ..\packages\StackExchange.Redis.1.0.488\lib\net45\StackExchange.Redis.dll - True + + ..\packages\StackExchange.Redis.1.2.3\lib\net45\StackExchange.Redis.dll + diff --git a/source/DoubleCache/packages.config b/source/DoubleCache/packages.config index 87e9ca3..3b2530e 100644 --- a/source/DoubleCache/packages.config +++ b/source/DoubleCache/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/source/DoubleCacheTests/DoubleCacheTests.csproj b/source/DoubleCacheTests/DoubleCacheTests.csproj index 6e12908..0a52c12 100644 --- a/source/DoubleCacheTests/DoubleCacheTests.csproj +++ b/source/DoubleCacheTests/DoubleCacheTests.csproj @@ -43,12 +43,12 @@ ..\packages\Shouldly.2.8.2\lib\net40\Shouldly.dll - - ..\packages\StackExchange.Redis.1.0.488\lib\net45\StackExchange.Redis.dll - True + + ..\packages\StackExchange.Redis.1.2.3\lib\net45\StackExchange.Redis.dll + diff --git a/source/DoubleCacheTests/packages.config b/source/DoubleCacheTests/packages.config index a84df33..a2c07fd 100644 --- a/source/DoubleCacheTests/packages.config +++ b/source/DoubleCacheTests/packages.config @@ -4,7 +4,7 @@ - + diff --git a/source/Sample/RandomUser/RandomUser.csproj b/source/Sample/RandomUser/RandomUser.csproj index 996782f..4623098 100644 --- a/source/Sample/RandomUser/RandomUser.csproj +++ b/source/Sample/RandomUser/RandomUser.csproj @@ -34,12 +34,12 @@ ..\..\packages\Newtonsoft.Json.8.0.1\lib\net45\Newtonsoft.Json.dll True - - ..\..\packages\StackExchange.Redis.1.0.488\lib\net45\StackExchange.Redis.dll - True + + ..\..\packages\StackExchange.Redis.1.2.3\lib\net45\StackExchange.Redis.dll + diff --git a/source/Sample/RandomUser/packages.config b/source/Sample/RandomUser/packages.config index a693d9b..88a5cbc 100644 --- a/source/Sample/RandomUser/packages.config +++ b/source/Sample/RandomUser/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/source/Sample/SampleApiCacheUpdateConsole/SampleApiCacheUpdateConsole.csproj b/source/Sample/SampleApiCacheUpdateConsole/SampleApiCacheUpdateConsole.csproj index ab85a26..4f7562e 100644 --- a/source/Sample/SampleApiCacheUpdateConsole/SampleApiCacheUpdateConsole.csproj +++ b/source/Sample/SampleApiCacheUpdateConsole/SampleApiCacheUpdateConsole.csproj @@ -37,12 +37,12 @@ ..\..\packages\Newtonsoft.Json.8.0.1\lib\net45\Newtonsoft.Json.dll True - - ..\..\packages\StackExchange.Redis.1.0.488\lib\net45\StackExchange.Redis.dll - True + + ..\..\packages\StackExchange.Redis.1.2.3\lib\net45\StackExchange.Redis.dll + diff --git a/source/Sample/SampleApiCacheUpdateConsole/packages.config b/source/Sample/SampleApiCacheUpdateConsole/packages.config index a693d9b..88a5cbc 100644 --- a/source/Sample/SampleApiCacheUpdateConsole/packages.config +++ b/source/Sample/SampleApiCacheUpdateConsole/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/source/Sample/SampleApiHostConsole/SampleApiHostConsole.csproj b/source/Sample/SampleApiHostConsole/SampleApiHostConsole.csproj index 4ca8725..787e066 100644 --- a/source/Sample/SampleApiHostConsole/SampleApiHostConsole.csproj +++ b/source/Sample/SampleApiHostConsole/SampleApiHostConsole.csproj @@ -53,12 +53,12 @@ ..\..\packages\Owin.1.0\lib\net40\Owin.dll True - - ..\..\packages\StackExchange.Redis.1.0.488\lib\net45\StackExchange.Redis.dll - True + + ..\..\packages\StackExchange.Redis.1.2.3\lib\net45\StackExchange.Redis.dll + ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll True diff --git a/source/Sample/SampleApiHostConsole/packages.config b/source/Sample/SampleApiHostConsole/packages.config index e24fe00..a107a1d 100644 --- a/source/Sample/SampleApiHostConsole/packages.config +++ b/source/Sample/SampleApiHostConsole/packages.config @@ -11,5 +11,5 @@ - + \ No newline at end of file diff --git a/source/Sample/SampleApiOwin/SampleApiOwin.csproj b/source/Sample/SampleApiOwin/SampleApiOwin.csproj index e95186b..8fbdf7b 100644 --- a/source/Sample/SampleApiOwin/SampleApiOwin.csproj +++ b/source/Sample/SampleApiOwin/SampleApiOwin.csproj @@ -62,12 +62,12 @@ ..\..\packages\Sigil.4.5.0\lib\net45\Sigil.dll True - - ..\..\packages\StackExchange.Redis.1.0.488\lib\net45\StackExchange.Redis.dll - True + + ..\..\packages\StackExchange.Redis.1.2.3\lib\net45\StackExchange.Redis.dll + ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll True diff --git a/source/Sample/SampleApiOwin/packages.config b/source/Sample/SampleApiOwin/packages.config index 14e2983..5a698a3 100644 --- a/source/Sample/SampleApiOwin/packages.config +++ b/source/Sample/SampleApiOwin/packages.config @@ -12,5 +12,5 @@ - + \ No newline at end of file From 7cfd50f28e537bce59665e5bdd3fcc008ea0fe28 Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Thu, 25 May 2017 13:39:17 +0200 Subject: [PATCH 10/14] Updated msgpack --- source/DoubleCache/DoubleCache.csproj | 5 ++--- source/DoubleCache/packages.config | 2 +- source/Sample/SampleApiOwin/SampleApiOwin.csproj | 5 ++--- source/Sample/SampleApiOwin/packages.config | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/source/DoubleCache/DoubleCache.csproj b/source/DoubleCache/DoubleCache.csproj index d806195..5c9be4c 100644 --- a/source/DoubleCache/DoubleCache.csproj +++ b/source/DoubleCache/DoubleCache.csproj @@ -30,9 +30,8 @@ 4 - - ..\packages\MsgPack.Cli.0.6.5\lib\net45\MsgPack.dll - True + + ..\packages\MsgPack.Cli.0.8.1\lib\net45\MsgPack.dll ..\packages\StackExchange.Redis.1.2.3\lib\net45\StackExchange.Redis.dll diff --git a/source/DoubleCache/packages.config b/source/DoubleCache/packages.config index 3b2530e..3ccd8b3 100644 --- a/source/DoubleCache/packages.config +++ b/source/DoubleCache/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/source/Sample/SampleApiOwin/SampleApiOwin.csproj b/source/Sample/SampleApiOwin/SampleApiOwin.csproj index 8fbdf7b..0060f4c 100644 --- a/source/Sample/SampleApiOwin/SampleApiOwin.csproj +++ b/source/Sample/SampleApiOwin/SampleApiOwin.csproj @@ -46,9 +46,8 @@ ..\..\packages\Microsoft.Owin.Hosting.2.0.2\lib\net45\Microsoft.Owin.Hosting.dll True - - ..\..\packages\MsgPack.Cli.0.6.5\lib\net45\MsgPack.dll - True + + ..\..\packages\MsgPack.Cli.0.8.1\lib\net45\MsgPack.dll ..\..\packages\Newtonsoft.Json.8.0.1\lib\net45\Newtonsoft.Json.dll diff --git a/source/Sample/SampleApiOwin/packages.config b/source/Sample/SampleApiOwin/packages.config index 5a698a3..c251f2c 100644 --- a/source/Sample/SampleApiOwin/packages.config +++ b/source/Sample/SampleApiOwin/packages.config @@ -8,7 +8,7 @@ - + From 919d665132627201e0deed4149154913ce5e0f54 Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Thu, 25 May 2017 13:44:11 +0200 Subject: [PATCH 11/14] Updated newtonsoft --- source/Sample/RandomUser/RandomUser.csproj | 5 ++--- source/Sample/RandomUser/packages.config | 2 +- .../SampleApiCacheUpdateConsole.csproj | 5 ++--- source/Sample/SampleApiCacheUpdateConsole/packages.config | 2 +- source/Sample/SampleApiHostConsole/App.config | 2 +- .../Sample/SampleApiHostConsole/SampleApiHostConsole.csproj | 5 ++--- source/Sample/SampleApiHostConsole/packages.config | 2 +- source/Sample/SampleApiOwin/SampleApiOwin.csproj | 5 ++--- source/Sample/SampleApiOwin/app.config | 2 +- source/Sample/SampleApiOwin/packages.config | 2 +- 10 files changed, 14 insertions(+), 18 deletions(-) diff --git a/source/Sample/RandomUser/RandomUser.csproj b/source/Sample/RandomUser/RandomUser.csproj index 4623098..4536bc0 100644 --- a/source/Sample/RandomUser/RandomUser.csproj +++ b/source/Sample/RandomUser/RandomUser.csproj @@ -30,9 +30,8 @@ 4 - - ..\..\packages\Newtonsoft.Json.8.0.1\lib\net45\Newtonsoft.Json.dll - True + + ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll ..\..\packages\StackExchange.Redis.1.2.3\lib\net45\StackExchange.Redis.dll diff --git a/source/Sample/RandomUser/packages.config b/source/Sample/RandomUser/packages.config index 88a5cbc..b33593b 100644 --- a/source/Sample/RandomUser/packages.config +++ b/source/Sample/RandomUser/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/source/Sample/SampleApiCacheUpdateConsole/SampleApiCacheUpdateConsole.csproj b/source/Sample/SampleApiCacheUpdateConsole/SampleApiCacheUpdateConsole.csproj index 4f7562e..4d42c53 100644 --- a/source/Sample/SampleApiCacheUpdateConsole/SampleApiCacheUpdateConsole.csproj +++ b/source/Sample/SampleApiCacheUpdateConsole/SampleApiCacheUpdateConsole.csproj @@ -33,9 +33,8 @@ 4 - - ..\..\packages\Newtonsoft.Json.8.0.1\lib\net45\Newtonsoft.Json.dll - True + + ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll ..\..\packages\StackExchange.Redis.1.2.3\lib\net45\StackExchange.Redis.dll diff --git a/source/Sample/SampleApiCacheUpdateConsole/packages.config b/source/Sample/SampleApiCacheUpdateConsole/packages.config index 88a5cbc..b33593b 100644 --- a/source/Sample/SampleApiCacheUpdateConsole/packages.config +++ b/source/Sample/SampleApiCacheUpdateConsole/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/source/Sample/SampleApiHostConsole/App.config b/source/Sample/SampleApiHostConsole/App.config index f9062de..447915d 100644 --- a/source/Sample/SampleApiHostConsole/App.config +++ b/source/Sample/SampleApiHostConsole/App.config @@ -7,7 +7,7 @@ - + diff --git a/source/Sample/SampleApiHostConsole/SampleApiHostConsole.csproj b/source/Sample/SampleApiHostConsole/SampleApiHostConsole.csproj index 787e066..6e65c7a 100644 --- a/source/Sample/SampleApiHostConsole/SampleApiHostConsole.csproj +++ b/source/Sample/SampleApiHostConsole/SampleApiHostConsole.csproj @@ -45,9 +45,8 @@ ..\..\packages\Microsoft.Owin.Hosting.2.0.2\lib\net45\Microsoft.Owin.Hosting.dll True - - ..\..\packages\Newtonsoft.Json.8.0.1\lib\net45\Newtonsoft.Json.dll - True + + ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll ..\..\packages\Owin.1.0\lib\net40\Owin.dll diff --git a/source/Sample/SampleApiHostConsole/packages.config b/source/Sample/SampleApiHostConsole/packages.config index a107a1d..f4b0b0c 100644 --- a/source/Sample/SampleApiHostConsole/packages.config +++ b/source/Sample/SampleApiHostConsole/packages.config @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/source/Sample/SampleApiOwin/SampleApiOwin.csproj b/source/Sample/SampleApiOwin/SampleApiOwin.csproj index 0060f4c..317c43d 100644 --- a/source/Sample/SampleApiOwin/SampleApiOwin.csproj +++ b/source/Sample/SampleApiOwin/SampleApiOwin.csproj @@ -49,9 +49,8 @@ ..\..\packages\MsgPack.Cli.0.8.1\lib\net45\MsgPack.dll - - ..\..\packages\Newtonsoft.Json.8.0.1\lib\net45\Newtonsoft.Json.dll - True + + ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll ..\..\packages\Owin.1.0\lib\net40\Owin.dll diff --git a/source/Sample/SampleApiOwin/app.config b/source/Sample/SampleApiOwin/app.config index 0f9899d..40b3291 100644 --- a/source/Sample/SampleApiOwin/app.config +++ b/source/Sample/SampleApiOwin/app.config @@ -8,7 +8,7 @@ - + diff --git a/source/Sample/SampleApiOwin/packages.config b/source/Sample/SampleApiOwin/packages.config index c251f2c..74e4887 100644 --- a/source/Sample/SampleApiOwin/packages.config +++ b/source/Sample/SampleApiOwin/packages.config @@ -9,7 +9,7 @@ - + From 2263c7035587682777b30210c47ced90875db993 Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Thu, 25 May 2017 13:46:57 +0200 Subject: [PATCH 12/14] Corrected version number --- build.fsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.fsx b/build.fsx index 435f830..7d89566 100644 --- a/build.fsx +++ b/build.fsx @@ -13,7 +13,7 @@ let references = !! "source/DoubleCache/*.csproj" ++ "source/DoubleCache.SystemWebCaching/DoubleCache.SystemWebCaching.csproj" let testReferences = !! "source/DoubleCacheTests/*.csproj" -let version = "2.0.0-beta.6" +let version = "2.0.0-beta6" let commitHash = Information.getCurrentSHA1(".") let projectName = "DoubleCache" From be4ce4a22172039a115715b1b3d1741b5fff3ba3 Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Thu, 25 May 2017 14:38:02 +0200 Subject: [PATCH 13/14] Added subscriber for existing items only. Fixes #18 --- README.md | 11 +- build.fsx | 6 +- .../DoubleCache.SystemWebCaching/HttpCache.cs | 5 + source/DoubleCache/DoubleCache.cs | 5 + source/DoubleCache/DoubleCache.csproj | 1 + .../ExistingItemSubscribingCache.cs | 112 +++++++++++ source/DoubleCache/ICacheAside.cs | 1 + source/DoubleCache/LocalCache/MemCache.cs | 5 + .../LocalCache/WrappingMemoryCache.cs | 5 + source/DoubleCache/Properties/AssemblyInfo.cs | 8 +- source/DoubleCache/PublishingCache.cs | 5 + source/DoubleCache/Redis/RedisCache.cs | 5 + source/DoubleCache/Redis/RedisStaleCache.cs | 5 + source/DoubleCache/SubscribingCache.cs | 5 + .../DoubleCacheTests/DoubleCacheTests.csproj | 1 + .../ExistingSubscribingCacheTests.cs | 180 ++++++++++++++++++ .../CacheImplementationTests.cs | 17 ++ 17 files changed, 365 insertions(+), 12 deletions(-) create mode 100644 source/DoubleCache/ExistingItemSubscribingCache.cs create mode 100644 source/DoubleCacheTests/ExistingSubscribingCacheTests.cs diff --git a/README.md b/README.md index 896b55a..440c7c5 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,13 @@ The Add and remove methods are implemented with fire and forget, hence it does n DoubleCache comes with the following implementations of this interface * *[obsolete]* LocalCache.MemCache - using System.Runtime.Memory, does not support null items. * LocalCache.WrappingMemoryCache - Allows storage of null items in Memory cache.* -* SystemWebCaching.HttpCache - An in memory cache using HttpContext.Cache -* Redis.RedisCache - using StackExchange.Redis client +* SystemWebCaching.HttpCache - An in memory cache using HttpContext.Cache. +* Redis.RedisCache - using StackExchange.Redis client. * Redis.RedisStaleCache - Use redis as a stale cache in order to mitigate cache stampede. -* SubscribingCache - a decorator supporting push notifications of cache updates -* PublishingCache - a decorator publishing cache changes -* DoubleCache - a decorator wrapping a local and a remote cache +* SubscribingCache - a decorator supporting push notifications of cache updates. +* ExistingItemSubscribingCache - a decorator supporting push notifications, which will only update cache if the item already exists. +* PublishingCache - a decorator publishing cache changes. +* DoubleCache - a decorator wrapping a local and a remote cache. \* using a custom proxy object holding the cache items. This is transparent to the client. diff --git a/build.fsx b/build.fsx index 7d89566..1d71026 100644 --- a/build.fsx +++ b/build.fsx @@ -13,7 +13,7 @@ let references = !! "source/DoubleCache/*.csproj" ++ "source/DoubleCache.SystemWebCaching/DoubleCache.SystemWebCaching.csproj" let testReferences = !! "source/DoubleCacheTests/*.csproj" -let version = "2.0.0-beta6" +let version = "2.0.0-beta7" let commitHash = Information.getCurrentSHA1(".") let projectName = "DoubleCache" @@ -67,8 +67,8 @@ Target "CreateNuget" (fun _ -> Version = version Publish = false Dependencies = [ - "StackExchange.Redis", "1.0.488" - "MsgPack.Cli", "0.6.5" + "StackExchange.Redis", "1.2.3" + "MsgPack.Cli", "0.8.1" ] Files = [ (@"DoubleCache.dll", Some @"lib\net45", None) diff --git a/source/DoubleCache.SystemWebCaching/HttpCache.cs b/source/DoubleCache.SystemWebCaching/HttpCache.cs index 5827228..e1a76f4 100644 --- a/source/DoubleCache.SystemWebCaching/HttpCache.cs +++ b/source/DoubleCache.SystemWebCaching/HttpCache.cs @@ -122,5 +122,10 @@ private DateTime CalculateExpire(TimeSpan? ttl) ? DateTime.Now.Add(ttl.Value) : DateTime.MaxValue; } + + public bool Exists(string key) + { + return _cache.Get(key) != null; + } } } diff --git a/source/DoubleCache/DoubleCache.cs b/source/DoubleCache/DoubleCache.cs index af77f7f..48d5d18 100644 --- a/source/DoubleCache/DoubleCache.cs +++ b/source/DoubleCache/DoubleCache.cs @@ -73,6 +73,11 @@ public void Remove(string key) _remoteCache.Remove(key); } + public bool Exists(string key) + { + return _localCache.Exists(key) && _remoteCache.Exists(key); + } + public TimeSpan? DefaultTtl { get { return _localCache.DefaultTtl > _remoteCache.DefaultTtl diff --git a/source/DoubleCache/DoubleCache.csproj b/source/DoubleCache/DoubleCache.csproj index 5c9be4c..dd5c11a 100644 --- a/source/DoubleCache/DoubleCache.csproj +++ b/source/DoubleCache/DoubleCache.csproj @@ -51,6 +51,7 @@ + diff --git a/source/DoubleCache/ExistingItemSubscribingCache.cs b/source/DoubleCache/ExistingItemSubscribingCache.cs new file mode 100644 index 0000000..755b0e9 --- /dev/null +++ b/source/DoubleCache/ExistingItemSubscribingCache.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace DoubleCache +{ + public class ExistingItemsSubscribingCache : ICacheAside + { + private readonly ICacheAside _cache; + private readonly ICacheSubscriber _cacheSubscriber; + private readonly ConcurrentDictionary _knownTypes; + + public ExistingItemsSubscribingCache(ICacheAside cache, ICacheSubscriber cacheSubscriber) + { + _knownTypes = new ConcurrentDictionary(); + _cache = cache; + _cacheSubscriber = cacheSubscriber; + + _cacheSubscriber.CacheUpdate += OnCacheUpdate; + _cacheSubscriber.CacheDelete += OnCacheDelete; + } + + public void Add(string key, T item) + { + _cache.Add(key, item); + } + + public void Add(string key, T item, TimeSpan? timeToLive) + { + _cache.Add(key, item, timeToLive); + } + + public T Get(string key, Func dataRetriever) where T : class + { + return _cache.Get(key, dataRetriever); + } + + public T Get(string key, Func dataRetriever, TimeSpan? timeToLive) where T : class + { + return _cache.Get(key, dataRetriever, timeToLive); + } + + public object Get(string key, Type type, Func dataRetriever) + { + return _cache.Get(key, type, dataRetriever); + } + + public object Get(string key, Type type, Func dataRetriever, TimeSpan? timeToLive) + { + return _cache.Get(key, type, dataRetriever, timeToLive); + } + + public Task GetAsync(string key, Type type, Func> dataRetriever) + { + return _cache.GetAsync(key, type, dataRetriever); + } + + public Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) + { + return _cache.GetAsync(key, type, dataRetriever, timeToLive); + } + + public Task GetAsync(string key, Func> dataRetriever) where T : class + { + return _cache.GetAsync(key, dataRetriever); + } + + public Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class + { + return _cache.GetAsync(key, dataRetriever, timeToLive); + } + + private async void OnCacheUpdate(object sender, CacheUpdateNotificationArgs e) + { + await CacheUpdateAction(sender, e).ConfigureAwait(false); + } + private async Task CacheUpdateAction(object sender, CacheUpdateNotificationArgs e) + { + + var type = _knownTypes.GetOrAdd(e.Type, Type.GetType(e.Type)); + if (_cache.Exists(e.Key)) + { + var remoteItem = await _cacheSubscriber.GetAsync(e.Key, type).ConfigureAwait(false); ; + + if (remoteItem != null) + { + if (e.SpecificTimeToLive != null) + Add(e.Key, remoteItem, e.SpecificTimeToLive._timeToLive); + else + Add(e.Key, remoteItem); + } + } + } + + private void OnCacheDelete(object sender, CacheUpdateNotificationArgs e) + { + Remove(e.Key); + } + + public void Remove(string key) + { + _cache.Remove(key); + } + + public bool Exists(string key) + { + return _cache.Exists(key); + } + + public TimeSpan? DefaultTtl { get { return _cache.DefaultTtl; } } + } +} diff --git a/source/DoubleCache/ICacheAside.cs b/source/DoubleCache/ICacheAside.cs index c5da507..a48dfda 100644 --- a/source/DoubleCache/ICacheAside.cs +++ b/source/DoubleCache/ICacheAside.cs @@ -22,6 +22,7 @@ public interface ICacheAside void Remove(string key); + bool Exists(string key); TimeSpan? DefaultTtl { get; } } } diff --git a/source/DoubleCache/LocalCache/MemCache.cs b/source/DoubleCache/LocalCache/MemCache.cs index 044b5d7..dcf74f7 100644 --- a/source/DoubleCache/LocalCache/MemCache.cs +++ b/source/DoubleCache/LocalCache/MemCache.cs @@ -102,6 +102,11 @@ public void Remove(string key) MemoryCache.Default.Remove(key); } + public bool Exists(string key) + { + return MemoryCache.Default.Get(key) != null; + } + public TimeSpan? DefaultTtl { get { return _defaultTtl; } } } } diff --git a/source/DoubleCache/LocalCache/WrappingMemoryCache.cs b/source/DoubleCache/LocalCache/WrappingMemoryCache.cs index 8b0e1d2..66d18a1 100644 --- a/source/DoubleCache/LocalCache/WrappingMemoryCache.cs +++ b/source/DoubleCache/LocalCache/WrappingMemoryCache.cs @@ -107,6 +107,11 @@ public void Remove(string key) MemoryCache.Default.Remove(key); } + public bool Exists(string key) + { + return MemoryCache.Default.Get(key) != null; + } + public TimeSpan? DefaultTtl { get { return _defaultTtl; } } } } diff --git a/source/DoubleCache/Properties/AssemblyInfo.cs b/source/DoubleCache/Properties/AssemblyInfo.cs index 3011a91..c1798c1 100644 --- a/source/DoubleCache/Properties/AssemblyInfo.cs +++ b/source/DoubleCache/Properties/AssemblyInfo.cs @@ -7,9 +7,9 @@ [assembly: GuidAttribute("505f87a8-3062-4070-af1f-cd7358ccd06a")] [assembly: AssemblyProductAttribute("DoubleCache")] [assembly: AssemblyVersionAttribute("2.0.0")] -[assembly: AssemblyInformationalVersionAttribute("2.0.0-beta.6")] +[assembly: AssemblyInformationalVersionAttribute("2.0.0-beta6")] [assembly: AssemblyFileVersionAttribute("2.0.0")] -[assembly: AssemblyMetadataAttribute("githash","dec9f54eb2a4442298e2681b6ba85c674addc4cc")] +[assembly: AssemblyMetadataAttribute("githash","2263c7035587682777b30210c47ced90875db993")] namespace System { internal static class AssemblyVersionInformation { internal const System.String AssemblyTitle = "DoubleCache"; @@ -17,8 +17,8 @@ internal static class AssemblyVersionInformation { internal const System.String Guid = "505f87a8-3062-4070-af1f-cd7358ccd06a"; internal const System.String AssemblyProduct = "DoubleCache"; internal const System.String AssemblyVersion = "2.0.0"; - internal const System.String AssemblyInformationalVersion = "2.0.0-beta.6"; + internal const System.String AssemblyInformationalVersion = "2.0.0-beta6"; internal const System.String AssemblyFileVersion = "2.0.0"; - internal const System.String AssemblyMetadata_githash = "dec9f54eb2a4442298e2681b6ba85c674addc4cc"; + internal const System.String AssemblyMetadata_githash = "2263c7035587682777b30210c47ced90875db993"; } } diff --git a/source/DoubleCache/PublishingCache.cs b/source/DoubleCache/PublishingCache.cs index 2281ec5..a8993ef 100644 --- a/source/DoubleCache/PublishingCache.cs +++ b/source/DoubleCache/PublishingCache.cs @@ -171,6 +171,11 @@ public void Remove(string key) _cachePublisher.NotifyDelete(key); } + public bool Exists(string key) + { + return _cache.Exists(key); + } + public TimeSpan? DefaultTtl { get { return _cache.DefaultTtl; } } } } diff --git a/source/DoubleCache/Redis/RedisCache.cs b/source/DoubleCache/Redis/RedisCache.cs index 64459f6..7a0ac93 100644 --- a/source/DoubleCache/Redis/RedisCache.cs +++ b/source/DoubleCache/Redis/RedisCache.cs @@ -117,6 +117,11 @@ public void Remove(string key) _database.KeyDelete(key); } + public bool Exists(string key) + { + return _database.KeyExists(key); + } + public TimeSpan? DefaultTtl { get { return _defaultTtl; } } } } diff --git a/source/DoubleCache/Redis/RedisStaleCache.cs b/source/DoubleCache/Redis/RedisStaleCache.cs index ce33a2e..32a0c05 100644 --- a/source/DoubleCache/Redis/RedisStaleCache.cs +++ b/source/DoubleCache/Redis/RedisStaleCache.cs @@ -183,6 +183,11 @@ public void Remove(string key) _redisCache.Remove(key); } + public bool Exists(string key) + { + return _redisCache.Exists(key); + } + public TimeSpan? DefaultTtl { get { return _redisCache.DefaultTtl; } } } } diff --git a/source/DoubleCache/SubscribingCache.cs b/source/DoubleCache/SubscribingCache.cs index 0427d61..903f4ef 100644 --- a/source/DoubleCache/SubscribingCache.cs +++ b/source/DoubleCache/SubscribingCache.cs @@ -97,6 +97,11 @@ public void Remove(string key) _cache.Remove(key); } + public bool Exists(string key) + { + return _cache.Exists(key); + } + public TimeSpan? DefaultTtl { get { return _cache.DefaultTtl; } } } } diff --git a/source/DoubleCacheTests/DoubleCacheTests.csproj b/source/DoubleCacheTests/DoubleCacheTests.csproj index 0a52c12..8570c35 100644 --- a/source/DoubleCacheTests/DoubleCacheTests.csproj +++ b/source/DoubleCacheTests/DoubleCacheTests.csproj @@ -71,6 +71,7 @@ + diff --git a/source/DoubleCacheTests/ExistingSubscribingCacheTests.cs b/source/DoubleCacheTests/ExistingSubscribingCacheTests.cs new file mode 100644 index 0000000..ff6088e --- /dev/null +++ b/source/DoubleCacheTests/ExistingSubscribingCacheTests.cs @@ -0,0 +1,180 @@ +using DoubleCache; +using FakeItEasy; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace DoubleCacheTests +{ + public class ExistingSubscribingCacheTests + { + private readonly ICacheAside _decoratedCache; + private readonly ICacheSubscriber _subscriber; + private readonly ICacheAside _subscribingCache; + + public ExistingSubscribingCacheTests() + { + _decoratedCache = A.Fake(); + _subscriber = A.Fake(); + _subscribingCache = new ExistingItemsSubscribingCache(_decoratedCache, _subscriber); + } + + + [Fact] + public void Add_CallsThrough() + { + _subscribingCache.Add("a", "b"); + + A.CallTo(() => _decoratedCache.Add("a", "b")).MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void Add_WithTtl_CallsThrough() + { + _subscribingCache.Add("a", "b", TimeSpan.FromMinutes(1)); + + A.CallTo(() => _decoratedCache.Add("a", "b", TimeSpan.FromMinutes(1))).MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void Get_CallsThrough() + { + _subscribingCache.Get("a", typeof(string), null); + A.CallTo(() => _decoratedCache.Get("a", typeof(string), A>._)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void Get_WithTimeToLive_CallsThrough() + { + _subscribingCache.Get("a", typeof(string), null, TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Get("a", typeof(string), A>._, TimeSpan.FromSeconds(1))) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void GetGeneric_CallsThrough() + { + _subscribingCache.Get("a", A.Fake>()); + A.CallTo(() => _decoratedCache.Get("a", A>._)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void GetGeneric_WithTimeToLive_CallsThrough() + { + _subscribingCache.Get("a", A.Fake>(), TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Get("a", A>._, TimeSpan.FromSeconds(1))) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task GetAsync_CallsThrough() + { + await _subscribingCache.GetAsync("a", typeof(string), null); + A.CallTo(() => _decoratedCache.GetAsync("a", typeof(string), A>>._)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task GetAsync_WithTimeToLive_CallsThrough() + { + await _subscribingCache.GetAsync("a", typeof(string), null, TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.GetAsync("a", typeof(string), A>>._, TimeSpan.FromSeconds(1))) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task GetAsyncGeneric_CallsThrough() + { + await _subscribingCache.GetAsync("a", A.Fake>>()); + A.CallTo(() => _decoratedCache.GetAsync("a", A>>._)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task GetAsyncGeneric_WithTimeToLive_CallsThrough() + { + await _subscribingCache.GetAsync("a", A.Fake>>(), TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.GetAsync("a", A>>._, TimeSpan.FromSeconds(1))) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public void Exists_CallsThrough() + { + _subscribingCache.Exists("a"); + A.CallTo(() => _decoratedCache.Exists("a")) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task SubscriberUpdate_ExistingItem_ItemAdded() + { + A.CallTo(() => _decoratedCache.Exists("a")).Returns(true); + A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); + _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName }); + + await Task.Delay(TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Add("a", "b")) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task SubscriberUpdate_UknownItem_NotAdded() + { + A.CallTo(() => _decoratedCache.Exists("a")).Returns(false); + A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); + _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName }); + + await Task.Delay(TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Add("a", "b")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task SubscriberUpdate_WithSpecificTimeToLive_null_ItemAddedWithTimeToLive() + { + A.CallTo(() => _decoratedCache.Exists("a")).Returns(true); + A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); + _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName, SpecificTimeToLive = new TimeToLive(null) }); + + await Task.Delay(TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Add("a", "b", null)) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task SubscriberUpdate_WithSpecificTimeToLive_value_ItemAddedWithTimeToLive() + { + A.CallTo(() => _decoratedCache.Exists("a")).Returns(true); + A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns("b"); + _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName, SpecificTimeToLive = new TimeToLive(TimeSpan.FromMinutes(1)) }); + + await Task.Delay(TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Add("a", "b", TimeSpan.FromMinutes(1))) + .MustHaveHappened(Repeated.Exactly.Once); + } + + [Fact] + public async Task SubscriberUpdater_NullReturned_DoesNotAddToCache() + { + A.CallTo(() => _subscriber.GetAsync("a", A.Ignored)).Returns(null); + _subscriber.CacheUpdate += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a", Type = typeof(string).AssemblyQualifiedName, SpecificTimeToLive = new TimeToLive(TimeSpan.FromMinutes(1)) }); + + await Task.Delay(TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Add("a", "b", TimeSpan.FromMinutes(1))) + .MustNotHaveHappened(); + } + + [Fact] + public async Task SubscriberDelete_ItemDelete() + { + _subscriber.CacheDelete += Raise.With(this, new CacheUpdateNotificationArgs { Key = "a" }); + + await Task.Delay(TimeSpan.FromSeconds(1)); + A.CallTo(() => _decoratedCache.Remove("a")) + .MustHaveHappened(Repeated.Exactly.Once); + } + } +} diff --git a/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs b/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs index 9c53b3f..122a350 100644 --- a/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs +++ b/source/DoubleCacheTests/IntegrationTests/CacheImplementationTests.cs @@ -275,5 +275,22 @@ public virtual void CacheWithTTL_Null_Returns_Null() result.ShouldBeNull(); } + + [Fact] + public void Exists_ItemDoesExsists_ReturnsTrue() + { + var key = Guid.NewGuid().ToString(); + _cacheImplementation.Add(key, "A"); + + _cacheImplementation.Exists(key).ShouldBeTrue(); + } + + [Fact] + public void Exists_ItemDoesNotExsists_ReturnsFalse() + { + var key = Guid.NewGuid().ToString(); + + _cacheImplementation.Exists(key).ShouldBeFalse(); + } } } From 35ffe0e42c13b24be5b27dbe2ec6e7affdee9a46 Mon Sep 17 00:00:00 2001 From: Harald Schult Ulriksen Date: Thu, 25 May 2017 15:39:10 +0200 Subject: [PATCH 14/14] Read TTL from Remote when updating local cache. Fixes #19 --- build.fsx | 2 +- source/DoubleCache/CacheFactory.cs | 3 +- source/DoubleCache/DoubleCache.cs | 21 ++-- source/DoubleCache/DoubleCache.csproj | 1 + source/DoubleCache/Properties/AssemblyInfo.cs | 8 +- source/DoubleCache/Redis/IKeyTimeToLive.cs | 11 ++ source/DoubleCache/Redis/RedisCache.cs | 12 ++- source/DoubleCacheTests/DoubleCacheTests.cs | 31 ++++-- .../SampleApiOwin/DoubleCacheController.cs | 100 +++++++++--------- 9 files changed, 116 insertions(+), 73 deletions(-) create mode 100644 source/DoubleCache/Redis/IKeyTimeToLive.cs diff --git a/build.fsx b/build.fsx index 1d71026..bae54b2 100644 --- a/build.fsx +++ b/build.fsx @@ -13,7 +13,7 @@ let references = !! "source/DoubleCache/*.csproj" ++ "source/DoubleCache.SystemWebCaching/DoubleCache.SystemWebCaching.csproj" let testReferences = !! "source/DoubleCacheTests/*.csproj" -let version = "2.0.0-beta7" +let version = "2.0.0-beta8" let commitHash = Information.getCurrentSHA1(".") let projectName = "DoubleCache" diff --git a/source/DoubleCache/CacheFactory.cs b/source/DoubleCache/CacheFactory.cs index b6ba1d1..177580b 100644 --- a/source/DoubleCache/CacheFactory.cs +++ b/source/DoubleCache/CacheFactory.cs @@ -12,7 +12,8 @@ public static ICacheAside CreatePubSubDoubleCache(IConnectionMultiplexer redisCo var remoteCache = new RedisCache(redisConnection.GetDatabase(), itemSerializer, defaultTtl); return new DoubleCache( new SubscribingCache(new LocalCache.WrappingMemoryCache(defaultTtl), new RedisSubscriber(redisConnection, remoteCache, itemSerializer)), - new PublishingCache(remoteCache, new RedisPublisher(redisConnection, itemSerializer))); + new PublishingCache(remoteCache, new RedisPublisher(redisConnection, itemSerializer)), + remoteCache); } } } diff --git a/source/DoubleCache/DoubleCache.cs b/source/DoubleCache/DoubleCache.cs index 48d5d18..9ddba89 100644 --- a/source/DoubleCache/DoubleCache.cs +++ b/source/DoubleCache/DoubleCache.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using DoubleCache.Redis; namespace DoubleCache { @@ -7,11 +8,13 @@ public class DoubleCache : ICacheAside { private readonly ICacheAside _localCache; private readonly ICacheAside _remoteCache; + private readonly IKeyTimeToLive _remoteTimeToLive; - public DoubleCache(ICacheAside localCache,ICacheAside remoteCache) + public DoubleCache(ICacheAside localCache,ICacheAside remoteCache, IKeyTimeToLive remoteTimeToLive) { _localCache = localCache; _remoteCache = remoteCache; + _remoteTimeToLive = remoteTimeToLive; } public void Add(string key, T item) @@ -28,43 +31,43 @@ public void Add(string key, T item, TimeSpan? timeToLive) public T Get(string key, Func dataRetriever) where T : class { - return _localCache.Get(key, () => _remoteCache.Get(key, dataRetriever)); + return _localCache.Get(key, () => _remoteCache.Get(key, dataRetriever), _remoteTimeToLive.KeyTimeToLive(key)); } public T Get(string key, Func dataRetriever, TimeSpan? timeToLive) where T : class { - return _localCache.Get(key, () => _remoteCache.Get(key, dataRetriever, timeToLive), timeToLive); + return _localCache.Get(key, () => _remoteCache.Get(key, dataRetriever, timeToLive), _remoteTimeToLive.KeyTimeToLive(key)); } public object Get(string key, Type type, Func dataRetriever) { - return _localCache.Get(key, type, () => _remoteCache.Get(key, type, dataRetriever)); + return _localCache.Get(key, type, () => _remoteCache.Get(key, type, dataRetriever), _remoteTimeToLive.KeyTimeToLive(key)); } public object Get(string key, Type type, Func dataRetriever, TimeSpan? timeToLive) { - return _localCache.Get(key, type, () => _remoteCache.Get(key, type, dataRetriever, timeToLive), timeToLive); + return _localCache.Get(key, type, () => _remoteCache.Get(key, type, dataRetriever, timeToLive), _remoteTimeToLive.KeyTimeToLive(key)); } public Task GetAsync(string key, Type type, Func> dataRetriever) { - return _localCache.GetAsync(key, type, () => _remoteCache.GetAsync(key, type, dataRetriever)); + return _localCache.GetAsync(key, type, () => _remoteCache.GetAsync(key, type, dataRetriever), _remoteTimeToLive.KeyTimeToLive(key)); } public Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) { - return _localCache.GetAsync(key, type, () => _remoteCache.GetAsync(key, type, dataRetriever), timeToLive); + return _localCache.GetAsync(key, type, () => _remoteCache.GetAsync(key, type, dataRetriever), _remoteTimeToLive.KeyTimeToLive(key)); } public Task GetAsync(string key, Func> dataRetriever) where T : class { - return _localCache.GetAsync(key, () => _remoteCache.GetAsync(key, dataRetriever)); + return _localCache.GetAsync(key, () => _remoteCache.GetAsync(key, dataRetriever), _remoteTimeToLive.KeyTimeToLive(key)); } public Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class { - return _localCache.GetAsync(key, () => _remoteCache.GetAsync(key, dataRetriever),timeToLive); + return _localCache.GetAsync(key, () => _remoteCache.GetAsync(key, dataRetriever, timeToLive), _remoteTimeToLive.KeyTimeToLive(key)); } public void Remove(string key) diff --git a/source/DoubleCache/DoubleCache.csproj b/source/DoubleCache/DoubleCache.csproj index dd5c11a..48923bf 100644 --- a/source/DoubleCache/DoubleCache.csproj +++ b/source/DoubleCache/DoubleCache.csproj @@ -59,6 +59,7 @@ + diff --git a/source/DoubleCache/Properties/AssemblyInfo.cs b/source/DoubleCache/Properties/AssemblyInfo.cs index c1798c1..6ff7bcc 100644 --- a/source/DoubleCache/Properties/AssemblyInfo.cs +++ b/source/DoubleCache/Properties/AssemblyInfo.cs @@ -7,9 +7,9 @@ [assembly: GuidAttribute("505f87a8-3062-4070-af1f-cd7358ccd06a")] [assembly: AssemblyProductAttribute("DoubleCache")] [assembly: AssemblyVersionAttribute("2.0.0")] -[assembly: AssemblyInformationalVersionAttribute("2.0.0-beta6")] +[assembly: AssemblyInformationalVersionAttribute("2.0.0-beta7")] [assembly: AssemblyFileVersionAttribute("2.0.0")] -[assembly: AssemblyMetadataAttribute("githash","2263c7035587682777b30210c47ced90875db993")] +[assembly: AssemblyMetadataAttribute("githash","be4ce4a22172039a115715b1b3d1741b5fff3ba3")] namespace System { internal static class AssemblyVersionInformation { internal const System.String AssemblyTitle = "DoubleCache"; @@ -17,8 +17,8 @@ internal static class AssemblyVersionInformation { internal const System.String Guid = "505f87a8-3062-4070-af1f-cd7358ccd06a"; internal const System.String AssemblyProduct = "DoubleCache"; internal const System.String AssemblyVersion = "2.0.0"; - internal const System.String AssemblyInformationalVersion = "2.0.0-beta6"; + internal const System.String AssemblyInformationalVersion = "2.0.0-beta7"; internal const System.String AssemblyFileVersion = "2.0.0"; - internal const System.String AssemblyMetadata_githash = "2263c7035587682777b30210c47ced90875db993"; + internal const System.String AssemblyMetadata_githash = "be4ce4a22172039a115715b1b3d1741b5fff3ba3"; } } diff --git a/source/DoubleCache/Redis/IKeyTimeToLive.cs b/source/DoubleCache/Redis/IKeyTimeToLive.cs new file mode 100644 index 0000000..591bbee --- /dev/null +++ b/source/DoubleCache/Redis/IKeyTimeToLive.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; + +namespace DoubleCache.Redis +{ + public interface IKeyTimeToLive + { + Task KeyTimeToLiveAsync(string key); + TimeSpan? KeyTimeToLive(string key); + } +} diff --git a/source/DoubleCache/Redis/RedisCache.cs b/source/DoubleCache/Redis/RedisCache.cs index 7a0ac93..af34a98 100644 --- a/source/DoubleCache/Redis/RedisCache.cs +++ b/source/DoubleCache/Redis/RedisCache.cs @@ -5,7 +5,7 @@ namespace DoubleCache.Redis { - public class RedisCache : ICacheAside + public class RedisCache : ICacheAside, IKeyTimeToLive { private readonly IDatabase _database; private readonly IItemSerializer _itemSerializer; @@ -122,6 +122,16 @@ public bool Exists(string key) return _database.KeyExists(key); } + public Task KeyTimeToLiveAsync(string key) + { + return _database.KeyTimeToLiveAsync(key); + } + + public TimeSpan? KeyTimeToLive(string key) + { + return _database.KeyTimeToLive(key); + } + public TimeSpan? DefaultTtl { get { return _defaultTtl; } } } } diff --git a/source/DoubleCacheTests/DoubleCacheTests.cs b/source/DoubleCacheTests/DoubleCacheTests.cs index c55d33c..508c5e1 100644 --- a/source/DoubleCacheTests/DoubleCacheTests.cs +++ b/source/DoubleCacheTests/DoubleCacheTests.cs @@ -3,6 +3,7 @@ using Xunit; using FakeItEasy; using DoubleCache; +using DoubleCache.Redis; namespace DoubleCacheTests { @@ -10,6 +11,7 @@ public class DoubleCacheTests { private readonly ICacheAside _local; private readonly ICacheAside _remote; + private readonly IKeyTimeToLive _remoteTTL; private readonly DoubleCache.DoubleCache _doubleCache; @@ -17,11 +19,13 @@ public DoubleCacheTests() { _local = A.Fake(); _remote = A.Fake(); + _remoteTTL = A.Fake(); _doubleCache = new DoubleCache.DoubleCache( _local, - _remote); + _remote, + _remoteTTL); } [Fact] @@ -47,16 +51,18 @@ public void Get_CalledOnLocal() { _doubleCache.Get("A", typeof(string), null); - A.CallTo(() => _local.Get("A", A.Ignored, A>.Ignored)).MustHaveHappened(Repeated.Exactly.Once); + A.CallTo(() => _local.Get("A", A.Ignored, A>.Ignored, A._)).MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _remote.Get("A", A.Ignored, A>.Ignored)).MustNotHaveHappened(); } [Fact] public void Get_WithTimeToLive_CalledOnLocal() { - _doubleCache.Get("A", typeof(string), null, TimeSpan.FromSeconds(1)); + A.CallTo(() => _remoteTTL.KeyTimeToLive("A")).Returns(TimeSpan.FromMilliseconds(800)); - A.CallTo(() => _local.Get("A", A.Ignored, A>.Ignored, TimeSpan.FromSeconds(1))).MustHaveHappened(Repeated.Exactly.Once); + _doubleCache.Get("A", typeof(string), null, TimeSpan.FromSeconds(1)); + + A.CallTo(() => _local.Get("A", A.Ignored, A>.Ignored, TimeSpan.FromMilliseconds(800))).MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _remote.Get("A", A.Ignored, A>.Ignored, TimeSpan.FromSeconds(1))).MustNotHaveHappened(); } @@ -65,13 +71,15 @@ public void GetGeneric_CalledOnLocal() { _doubleCache.Get("A", null); - A.CallTo(() => _local.Get("A", A>.Ignored)).MustHaveHappened(Repeated.Exactly.Once); + A.CallTo(() => _local.Get("A", A>.Ignored,A._)).MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _remote.Get("A", A>.Ignored)).MustNotHaveHappened(); } [Fact] public void GetGeneric_WithTimeToLive_CalledOnLocal() { + A.CallTo(() => _remoteTTL.KeyTimeToLive("A")).Returns(TimeSpan.FromSeconds(1)); + _doubleCache.Get("A", null, TimeSpan.FromSeconds(1)); A.CallTo(() => _local.Get("A", A>.Ignored, TimeSpan.FromSeconds(1))).MustHaveHappened(Repeated.Exactly.Once); @@ -81,18 +89,23 @@ public void GetGeneric_WithTimeToLive_CalledOnLocal() [Fact] public async Task GetAsync_CalledOnLocal() { + A.CallTo(() => _remoteTTL.KeyTimeToLive("A")).Returns(TimeSpan.FromSeconds(1)); + + await _doubleCache.GetAsync("A", typeof(string), null).ConfigureAwait(false); - A.CallTo(() => _local.GetAsync("A", A.Ignored, A>>.Ignored)).MustHaveHappened(Repeated.Exactly.Once); + A.CallTo(() => _local.GetAsync("A", A.Ignored, A>>.Ignored, A._)).MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _remote.GetAsync("A", A.Ignored, A>>.Ignored)).MustNotHaveHappened(); } [Fact] public async Task GetAsync_WithTimeToLive_CalledOnLocal() { + A.CallTo(() => _remoteTTL.KeyTimeToLive("A")).Returns(TimeSpan.FromMilliseconds(800)); + await _doubleCache.GetAsync("A", typeof(string), null, TimeSpan.FromSeconds(1)); - A.CallTo(() => _local.GetAsync("A", A.Ignored, A>>.Ignored, TimeSpan.FromSeconds(1))).MustHaveHappened(Repeated.Exactly.Once); + A.CallTo(() => _local.GetAsync("A", A.Ignored, A>>.Ignored, TimeSpan.FromMilliseconds(800))).MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _remote.GetAsync("A", A.Ignored, A>>.Ignored, TimeSpan.FromSeconds(1))).MustNotHaveHappened(); } @@ -101,13 +114,15 @@ public async Task GetAsyncGeneric_CalledOnLocal() { await _doubleCache.GetAsync("A", null); - A.CallTo(() => _local.GetAsync("A", A>>.Ignored)).MustHaveHappened(Repeated.Exactly.Once); + A.CallTo(() => _local.GetAsync("A", A>>.Ignored, A._)).MustHaveHappened(Repeated.Exactly.Once); A.CallTo(() => _remote.GetAsync("A", A>>.Ignored)).MustNotHaveHappened(); } [Fact] public async Task GetAsyncGeneric_WithTimeToLive_CalledOnLocal() { + A.CallTo(() => _remoteTTL.KeyTimeToLive("A")).Returns(TimeSpan.FromSeconds(1)); + await _doubleCache.GetAsync("A", null, TimeSpan.FromSeconds(1)); A.CallTo(() => _local.GetAsync("A", A>>.Ignored, TimeSpan.FromSeconds(1))).MustHaveHappened(Repeated.Exactly.Once); diff --git a/source/Sample/SampleApiOwin/DoubleCacheController.cs b/source/Sample/SampleApiOwin/DoubleCacheController.cs index 7dcbac1..ca3718d 100644 --- a/source/Sample/SampleApiOwin/DoubleCacheController.cs +++ b/source/Sample/SampleApiOwin/DoubleCacheController.cs @@ -1,54 +1,56 @@ -using DoubleCache; -using DoubleCache.Redis; -using DoubleCache.Serialization; -using RandomUser; -using StackExchange.Redis; -using System.Threading.Tasks; -using System.Web.Http; -using System.Net.Http; -using System.Net; - -namespace CacheSample -{ - [RoutePrefix("doublecache")] - public class DoubleCacheController : ApiController, IUserController - { - private static ICacheAside _doubleCache; - private RandomUserRepository _repo; - - static DoubleCacheController() - { - _doubleCache = new DoubleCache.DoubleCache( - new DoubleCache.LocalCache.MemCache(), - new RedisCache(ConnectionMultiplexer.Connect("localhost").GetDatabase(), new MsgPackItemSerializer())); - } - - public DoubleCacheController() - { - _repo = new RandomUserRepository(); - } - - [Route("single")] - public async Task GetSingle() - { - return Ok(await _doubleCache.GetAsync(Request.RequestUri.PathAndQuery, () => _repo.GetSingleDummyUser())); - } - - [Route("many")] - public async Task GetMany() - { - return Ok(await _doubleCache.GetAsync(Request.RequestUri.PathAndQuery, () => _repo.GetManyDummyUser(100))); - } - - [HttpDelete] - [Route("single")] - [Route("many")] +using DoubleCache; +using DoubleCache.Redis; +using DoubleCache.Serialization; +using RandomUser; +using StackExchange.Redis; +using System.Threading.Tasks; +using System.Web.Http; +using System.Net.Http; +using System.Net; + +namespace CacheSample +{ + [RoutePrefix("doublecache")] + public class DoubleCacheController : ApiController, IUserController + { + private static ICacheAside _doubleCache; + private RandomUserRepository _repo; + + static DoubleCacheController() + { + var remoteCache = new RedisCache(ConnectionMultiplexer.Connect("localhost").GetDatabase(), new MsgPackItemSerializer()); + _doubleCache = new DoubleCache.DoubleCache( + new DoubleCache.LocalCache.WrappingMemoryCache(), + remoteCache, + remoteCache); + } + + public DoubleCacheController() + { + _repo = new RandomUserRepository(); + } + + [Route("single")] + public async Task GetSingle() + { + return Ok(await _doubleCache.GetAsync(Request.RequestUri.PathAndQuery, () => _repo.GetSingleDummyUser())); + } + + [Route("many")] + public async Task GetMany() + { + return Ok(await _doubleCache.GetAsync(Request.RequestUri.PathAndQuery, () => _repo.GetManyDummyUser(100))); + } + + [HttpDelete] + [Route("single")] + [Route("many")] public IHttpActionResult Remove() { _doubleCache.Remove(Request.RequestUri.PathAndQuery); return ResponseMessage(new HttpResponseMessage(HttpStatusCode.NoContent)); - } - } -} - + } + } +} +