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. + diff --git a/build.fsx b/build.fsx index f142e70..1fe0269 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.0" +let version = "2.0.0-beta4" let commitHash = Information.getCurrentSHA1(".") let projectName = "DoubleCache" diff --git a/source/DoubleCache.SystemWebCaching/HttpCache.cs b/source/DoubleCache.SystemWebCaching/HttpCache.cs index cf333e7..5827228 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 @@ -68,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; } @@ -85,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/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))); } } 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/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..044b5d7 100644 --- a/source/DoubleCache/LocalCache/MemCache.cs +++ b/source/DoubleCache/LocalCache/MemCache.cs @@ -4,10 +4,11 @@ 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; - + public MemCache(TimeSpan? defaultTtl = null) { _defaultTtl = defaultTtl; @@ -74,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; } @@ -90,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 new file mode 100644 index 0000000..8b0e1d2 --- /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().ConfigureAwait(false); + Add(key, item, timeToLive); + return item == null || 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().ConfigureAwait(false); + 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/DoubleCache/Properties/AssemblyInfo.cs b/source/DoubleCache/Properties/AssemblyInfo.cs index dc2b2c2..d544560 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.5.0")] -[assembly: AssemblyInformationalVersionAttribute("1.5.0")] -[assembly: AssemblyFileVersionAttribute("1.5.0")] -[assembly: AssemblyMetadataAttribute("githash","54c5137866e8115563eb483c019c1ee0fb4c509e")] +[assembly: AssemblyVersionAttribute("2.0.0")] +[assembly: AssemblyInformationalVersionAttribute("2.0.0-beta2")] +[assembly: AssemblyFileVersionAttribute("2.0.0")] +[assembly: AssemblyMetadataAttribute("githash","859b9c2fc37a55e99a1e40abee11d15d1118405c")] 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.5.0"; - internal const System.String AssemblyInformationalVersion = "1.5.0"; - internal const System.String AssemblyFileVersion = "1.5.0"; - internal const System.String AssemblyMetadata_githash = "54c5137866e8115563eb483c019c1ee0fb4c509e"; + internal const System.String AssemblyVersion = "2.0.0"; + internal const System.String AssemblyInformationalVersion = "2.0.0-beta2"; + internal const System.String AssemblyFileVersion = "2.0.0"; + internal const System.String AssemblyMetadata_githash = "859b9c2fc37a55e99a1e40abee11d15d1118405c"; } } diff --git a/source/DoubleCache/PublishingCache.cs b/source/DoubleCache/PublishingCache.cs index 5a18699..c9cb616 100644 --- a/source/DoubleCache/PublishingCache.cs +++ b/source/DoubleCache/PublishingCache.cs @@ -17,20 +17,20 @@ 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); } public T Get(string key, Func dataRetriever) where T : class { return _cache.Get(key, () => { var result = dataRetriever.Invoke(); - _cachePublisher.NotifyUpdate(key, result.GetType().AssemblyQualifiedName); + _cachePublisher.NotifyUpdate(key, typeof(T).AssemblyQualifiedName); return result; }); } @@ -39,7 +39,8 @@ public T Get(string key, Func dataRetriever, TimeSpan? timeToLive) where T { return _cache.Get(key, () => { var result = dataRetriever.Invoke(); - _cachePublisher.NotifyUpdate(key, result.GetType().AssemblyQualifiedName, timeToLive); + var name = typeof(T).AssemblyQualifiedName; + _cachePublisher.NotifyUpdate(key, name, timeToLive); return result; }, timeToLive); } @@ -48,7 +49,7 @@ public object Get(string key, Type type, Func dataRetriever) { return _cache.Get(key, type, () => { var result = dataRetriever.Invoke(); - _cachePublisher.NotifyUpdate(key, result.GetType().AssemblyQualifiedName); + _cachePublisher.NotifyUpdate(key, type.AssemblyQualifiedName); return result; }); } @@ -57,7 +58,7 @@ public object Get(string key, Type type, Func dataRetriever, TimeSpan? t { return _cache.Get(key, type, () => { var result = dataRetriever.Invoke(); - _cachePublisher.NotifyUpdate(key, result.GetType().AssemblyQualifiedName, timeToLive); + _cachePublisher.NotifyUpdate(key,type.AssemblyQualifiedName, timeToLive); return result; }, timeToLive); } @@ -65,7 +66,7 @@ public object Get(string key, Type type, Func dataRetriever, TimeSpan? t public Task GetAsync(string key, Type type, Func> dataRetriever) { Func> wrappedAction = async () => { - var result = await dataRetriever.Invoke(); + var result = await dataRetriever.Invoke().ConfigureAwait(false); var qualifiedTypeName = result.GetType().AssemblyQualifiedName; _cachePublisher.NotifyUpdate(key, qualifiedTypeName); return result; @@ -77,8 +78,8 @@ public Task GetAsync(string key, Type type, Func> dataRetri public Task GetAsync(string key, Type type, Func> dataRetriever, TimeSpan? timeToLive) { Func> wrappedAction = async () => { - var result = await dataRetriever.Invoke(); - var qualifiedTypeName = result.GetType().AssemblyQualifiedName; + var result = await dataRetriever.Invoke().ConfigureAwait(false); + var qualifiedTypeName = type.AssemblyQualifiedName; _cachePublisher.NotifyUpdate(key, qualifiedTypeName); return result; }; @@ -89,17 +90,18 @@ public Task GetAsync(string key, Type type, Func> dataRetri public Task GetAsync(string key, Func> dataRetriever) where T : class { return _cache.GetAsync(key, async() => { - var result = await dataRetriever.Invoke(); - _cachePublisher.NotifyUpdate(key, result.GetType().AssemblyQualifiedName); + var result = await dataRetriever.Invoke().ConfigureAwait(false); + _cachePublisher.NotifyUpdate(key, typeof(T).AssemblyQualifiedName); return result; }); } public Task GetAsync(string key, Func> dataRetriever, TimeSpan? timeToLive) where T : class { - return _cache.GetAsync(key, async () => { - var result = await dataRetriever.Invoke(); - _cachePublisher.NotifyUpdate(key, result.GetType().AssemblyQualifiedName, timeToLive); + return _cache.GetAsync(key, async () => + { + var result = await dataRetriever.Invoke().ConfigureAwait(false); + _cachePublisher.NotifyUpdate(key, typeof(T).AssemblyQualifiedName, timeToLive); return result; }, timeToLive); } 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/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/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(); diff --git a/source/DoubleCacheTests/DoubleCacheTests.csproj b/source/DoubleCacheTests/DoubleCacheTests.csproj index 8b20834..0a84175 100644 --- a/source/DoubleCacheTests/DoubleCacheTests.csproj +++ b/source/DoubleCacheTests/DoubleCacheTests.csproj @@ -78,7 +78,9 @@ + + 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 ae91650..f9b5eeb 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 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 new file mode 100644 index 0000000..8c34626 --- /dev/null +++ b/source/DoubleCacheTests/IntegrationTests/WrappingMemoryCacheTests.cs @@ -0,0 +1,17 @@ +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)); + } + } +} 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); + } + } } }