diff --git a/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.cs b/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.cs index d0876868..b77591db 100644 --- a/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.cs +++ b/src/EasyCaching.Memcached/DefaultMemcachedCachingProvider.cs @@ -4,6 +4,8 @@ using EasyCaching.Core.Internal; using Enyim.Caching; using System; + using System.Security.Cryptography; + using System.Text; using System.Threading.Tasks; /// @@ -44,7 +46,7 @@ public CacheValue Get(string cacheKey, Func dataRetriever, TimeSpan exp ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); - var result = _memcachedClient.Get(cacheKey) as T; + var result = _memcachedClient.Get(this.HandleCacheKey(cacheKey)) as T; if (result != null) { return new CacheValue(result, true); @@ -53,7 +55,7 @@ public CacheValue Get(string cacheKey, Func dataRetriever, TimeSpan exp var item = dataRetriever?.Invoke(); if (item != null) { - Set(cacheKey, item, expiration); + this.Set(cacheKey, item, expiration); return new CacheValue(item, true); } else @@ -75,7 +77,7 @@ public async Task> GetAsync(string cacheKey, Func> data ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); - var result = await _memcachedClient.GetValueAsync(cacheKey); + var result = await _memcachedClient.GetValueAsync(this.HandleCacheKey(cacheKey)); if (result != null) { return new CacheValue(result, true); @@ -84,7 +86,7 @@ public async Task> GetAsync(string cacheKey, Func> data var item = await dataRetriever?.Invoke(); if (item != null) { - await SetAsync(cacheKey, item, expiration); + await this.SetAsync(cacheKey, item, expiration); return new CacheValue(item, true); } else @@ -103,7 +105,7 @@ public CacheValue Get(string cacheKey) where T : class { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); - var result = _memcachedClient.Get(cacheKey) as T; + var result = _memcachedClient.Get(this.HandleCacheKey(cacheKey)) as T; if (result != null) { return new CacheValue(result, true); @@ -122,9 +124,9 @@ public CacheValue Get(string cacheKey) where T : class /// The 1st type parameter. public async Task> GetAsync(string cacheKey) where T : class { - ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); + ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); - var result = await _memcachedClient.GetValueAsync(cacheKey); + var result = await _memcachedClient.GetValueAsync(this.HandleCacheKey(cacheKey)); if (result != null) { return new CacheValue(result, true); @@ -144,7 +146,7 @@ public void Remove(string cacheKey) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); - _memcachedClient.Remove(cacheKey); + _memcachedClient.Remove(this.HandleCacheKey(cacheKey)); } /// @@ -156,7 +158,7 @@ public async Task RemoveAsync(string cacheKey) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); - await _memcachedClient.RemoveAsync(cacheKey); + await _memcachedClient.RemoveAsync(this.HandleCacheKey(cacheKey)); } /// @@ -173,7 +175,7 @@ public void Set(string cacheKey, T cacheValue, TimeSpan expiration) where T : ArgumentCheck.NotNull(cacheValue, nameof(cacheValue)); ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); - _memcachedClient.Add(cacheKey, cacheValue, expiration.Seconds); + _memcachedClient.Store(Enyim.Caching.Memcached.StoreMode.Set, this.HandleCacheKey(cacheKey), cacheValue, expiration); } /// @@ -190,7 +192,7 @@ public async Task SetAsync(string cacheKey, T cacheValue, TimeSpan expiration ArgumentCheck.NotNull(cacheValue, nameof(cacheValue)); ArgumentCheck.NotNegativeOrZero(expiration, nameof(expiration)); - await _memcachedClient.AddAsync(cacheKey, cacheValue, expiration.Seconds); + await _memcachedClient.StoreAsync(Enyim.Caching.Memcached.StoreMode.Set, this.HandleCacheKey(cacheKey), cacheValue, expiration); } /// @@ -202,7 +204,7 @@ public bool Exists(string cacheKey) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); - return _memcachedClient.TryGet(cacheKey, out object obj); + return _memcachedClient.TryGet(this.HandleCacheKey(cacheKey), out object obj); } /// @@ -214,7 +216,7 @@ public Task ExistsAsync(string cacheKey) { ArgumentCheck.NotNullOrWhiteSpace(cacheKey, nameof(cacheKey)); - return Task.Run(() => { return _memcachedClient.TryGet(cacheKey, out object obj); }); + return Task.Run(() => { return _memcachedClient.TryGet(this.HandleCacheKey(cacheKey), out object obj); }); } /// @@ -252,14 +254,70 @@ public async Task RefreshAsync(string cacheKey, T cacheValue, TimeSpan expira await this.SetAsync(cacheKey, cacheValue, expiration); } + /// + /// Removes cached item by cachekey's prefix. + /// + /// + /// Before using the method , you should follow this link + /// https://github.com/memcached/memcached/wiki/ProgrammingTricks#namespacing + /// and confirm that you use the namespacing when you set and get the cache. + /// + /// Prefix of CacheKey. public void RemoveByPrefix(string prefix) { - throw new NotImplementedException(); + var oldPrefixKey = _memcachedClient.Get(prefix)?.ToString(); + + var newValue = DateTime.UtcNow.Ticks.ToString(); + + if (oldPrefixKey.Equals(newValue)) + { + newValue = string.Concat(newValue, new Random().Next(9).ToString()); + } + _memcachedClient.Store(Enyim.Caching.Memcached.StoreMode.Set, this.HandleCacheKey(prefix), newValue, new TimeSpan(0, 0, 0)); } - public Task RemoveByPrefixAsync(string prefix) + /// + /// Removes cached item by cachekey's prefix async. + /// + /// + /// Before using the method , you should follow this link + /// https://github.com/memcached/memcached/wiki/ProgrammingTricks#namespacing + /// and confirm that you use the namespacing when you set and get the cache. + /// + /// Prefix of CacheKey. + /// + public async Task RemoveByPrefixAsync(string prefix) { - throw new NotImplementedException(); + var oldPrefixKey = _memcachedClient.Get(prefix)?.ToString(); + + var newValue = DateTime.UtcNow.Ticks.ToString(); + + if (oldPrefixKey.Equals(newValue)) + { + newValue = string.Concat(newValue, new Random().Next(9).ToString()); + } + await _memcachedClient.StoreAsync(Enyim.Caching.Memcached.StoreMode.Set, this.HandleCacheKey(prefix), newValue, new TimeSpan(0, 0, 0)); + } + + /// + /// Handle the cache key of memcached limititaion + /// + /// Cache Key + /// + private string HandleCacheKey(string cacheKey) + { + // Memcached has a 250 character limit + // Following memcached.h in https://github.com/memcached/memcached/ + if (cacheKey.Length >= 250) + { + using (SHA1 sha1 = SHA1.Create()) + { + byte[] data = sha1.ComputeHash(Encoding.UTF8.GetBytes(cacheKey)); + return Convert.ToBase64String(data, Base64FormattingOptions.None); + } + } + + return cacheKey; } } } diff --git a/test/EasyCaching.UnitTests/CachingTests/MemcachedProviderTest.cs b/test/EasyCaching.UnitTests/CachingTests/MemcachedProviderTest.cs index ac3499fb..5bff2c87 100644 --- a/test/EasyCaching.UnitTests/CachingTests/MemcachedProviderTest.cs +++ b/test/EasyCaching.UnitTests/CachingTests/MemcachedProviderTest.cs @@ -174,5 +174,77 @@ public async Task Refresh_Async_Should_Succeed() Assert.Equal("NewValue", act.Value); } + + [Fact] + public void RemoveByPrefix_Should_Succeed() + { + string prefixKey = "demo"; + string prefixValue = "abc"; + + _provider.Set(prefixKey, prefixValue, TimeSpan.FromSeconds(120)); + + + SetCacheItem("1", "1", prefixKey); + SetCacheItem("2", "2", prefixKey); + SetCacheItem("3", "3", prefixKey); + SetCacheItem("4", "4", prefixKey); + + _provider.RemoveByPrefix(prefixKey); + + GetCacheItem("1", prefixKey); + GetCacheItem("2", prefixKey); + GetCacheItem("3", prefixKey); + GetCacheItem("4", prefixKey); + + var afterPrefixValue = _provider.Get(prefixKey); + Assert.NotEqual(prefixValue, afterPrefixValue.Value); + } + + [Fact] + public async Task RemoveByPrefix_Async_Should_Succeed() + { + string prefixKey = "demo"; + string prefixValue = "abc"; + + _provider.Set("demo", prefixValue, TimeSpan.FromSeconds(120)); + + SetCacheItem("1", "1", prefixKey); + SetCacheItem("2", "2", prefixKey); + SetCacheItem("3", "3", prefixKey); + SetCacheItem("4", "4", prefixKey); + + await _provider.RemoveByPrefixAsync(prefixKey); + + GetCacheItem("1", prefixKey); + GetCacheItem("2", prefixKey); + GetCacheItem("3", prefixKey); + GetCacheItem("4", prefixKey); + + var afterPrefixValue = _provider.Get(prefixKey); + Assert.NotEqual(prefixValue, afterPrefixValue.Value); + } + + private void SetCacheItem(string cacheKey, string cacheValue, string prefix) + { + var pre = _provider.Get(prefix); + + cacheKey = string.Concat(pre, cacheKey); + + _provider.Set(cacheKey, cacheValue, _defaultTs); + + var val = _provider.Get(cacheKey); + Assert.Equal(cacheValue, val.Value); + } + + + private void GetCacheItem(string cacheKey, string prefix) + { + var pre = _provider.Get(prefix); + + cacheKey = string.Concat(pre, cacheKey); + + var val = _provider.Get(cacheKey); + Assert.False(val.HasValue); + } } }