diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs index 48d55cc0d5..0502cd8d81 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs @@ -21,6 +21,7 @@ namespace Volo.Abp.Caching context.Services.AddDistributedMemoryCache(); context.Services.AddSingleton(typeof(IDistributedCache<>), typeof(DistributedCache<>)); + context.Services.AddSingleton(typeof(IDistributedCache<,>), typeof(DistributedCache<,>)); context.Services.Configure(cacheOptions => { diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs index caa51aeac1..654e60f323 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -439,4 +439,327 @@ namespace Volo.Abp.Caching DefaultCacheOptions = GetDefaultCacheEntryOptions(); } } + /// + /// Represents a distributed cache of type. + /// Uses a generic cache key type of type. + /// + /// The type of cache item being cached. + /// The type of cache key being used. + public class DistributedCache : IDistributedCache + where TCacheItem : class + { + public ILogger> Logger { get; set; } + + protected string CacheName { get; set; } + + protected bool IgnoreMultiTenancy { get; set; } + + protected IDistributedCache Cache { get; } + + protected ICancellationTokenProvider CancellationTokenProvider { get; } + + protected IDistributedCacheSerializer Serializer { get; } + + protected ICurrentTenant CurrentTenant { get; } + + protected SemaphoreSlim SyncSemaphore { get; } + + protected DistributedCacheEntryOptions DefaultCacheOptions; + + private readonly CacheOptions _cacheOption; + + private readonly DistributedCacheOptions _distributedCacheOption; + + public DistributedCache( + IOptions cacheOption, + IOptions distributedCacheOption, + IDistributedCache cache, + ICancellationTokenProvider cancellationTokenProvider, + IDistributedCacheSerializer serializer, + ICurrentTenant currentTenant) + { + _distributedCacheOption = distributedCacheOption.Value; + _cacheOption = cacheOption.Value; + Cache = cache; + CancellationTokenProvider = cancellationTokenProvider; + Logger = NullLogger>.Instance; + Serializer = serializer; + CurrentTenant = currentTenant; + + SyncSemaphore = new SemaphoreSlim(1, 1); + + SetDefaultOptions(); + } + protected virtual string NormalizeKey(string key) + { + var normalizedKey = "c:" + CacheName + ",k:" + _cacheOption.KeyPrefix + key; + + if (!IgnoreMultiTenancy && CurrentTenant.Id.HasValue) + { + normalizedKey = "t:" + CurrentTenant.Id.Value + "," + normalizedKey; + } + + return normalizedKey; + } + + protected virtual DistributedCacheEntryOptions GetDefaultCacheEntryOptions() + { + foreach (var configure in _cacheOption.CacheConfigurators) + { + var options = configure.Invoke(CacheName); + if (options != null) + { + return options; + } + } + return _cacheOption.GlobalCacheEntryOptions; + } + + protected virtual void SetDefaultOptions() + { + CacheName = CacheNameAttribute.GetCacheName(typeof(TCacheItem)); + + //IgnoreMultiTenancy + IgnoreMultiTenancy = typeof(TCacheItem).IsDefined(typeof(IgnoreMultiTenancyAttribute), true); + + //Configure default cache entry options + DefaultCacheOptions = GetDefaultCacheEntryOptions(); + } + public virtual TCacheItem Get(TCacheKey key, bool? hideErrors = null) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + byte[] cachedBytes; + + try + { + cachedBytes = Cache.Get(NormalizeKey(key?.ToString())); + } + catch (Exception ex) + { + if (hideErrors == true) + { + Logger.LogException(ex, LogLevel.Warning); + return null; + } + + throw; + } + + if (cachedBytes == null) + { + return null; + } + + return Serializer.Deserialize(cachedBytes); + } + + public virtual async Task GetAsync(TCacheKey key, bool? hideErrors = null, CancellationToken token = default) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + byte[] cachedBytes; + + try + { + cachedBytes = await Cache.GetAsync( + NormalizeKey(key.ToString()), + CancellationTokenProvider.FallbackToProvider(token) + ); + } + catch (Exception ex) + { + if (hideErrors == true) + { + Logger.LogException(ex, LogLevel.Warning); + return null; + } + + throw; + } + + if (cachedBytes == null) + { + return null; + } + + return Serializer.Deserialize(cachedBytes); + } + + public virtual TCacheItem GetOrAdd(TCacheKey key, Func factory, Func optionsFactory = null, bool? hideErrors = null) + { + var value = Get(key, hideErrors); + if (value != null) + { + return value; + } + + using (SyncSemaphore.Lock()) + { + value = Get(key, hideErrors); + if (value != null) + { + return value; + } + + value = factory(); + Set(key, value, optionsFactory?.Invoke(), hideErrors); + } + + return value; + } + + public virtual async Task GetOrAddAsync(TCacheKey key, Func> factory, Func optionsFactory = null, bool? hideErrors = null, CancellationToken token = default) + { + token = CancellationTokenProvider.FallbackToProvider(token); + var value = await GetAsync(key, hideErrors, token); + if (value != null) + { + return value; + } + + using (await SyncSemaphore.LockAsync(token)) + { + value = await GetAsync(key, hideErrors, token); + if (value != null) + { + return value; + } + + value = await factory(); + await SetAsync(key, value, optionsFactory?.Invoke(), hideErrors, token); + } + + return value; + } + + public virtual void Set(TCacheKey key, TCacheItem value, DistributedCacheEntryOptions options = null, bool? hideErrors = null) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + Cache.Set( + NormalizeKey(key.ToString()), + Serializer.Serialize(value), + options ?? DefaultCacheOptions + ); + } + catch (Exception ex) + { + if (hideErrors == true) + { + Logger.LogException(ex, LogLevel.Warning); + return; + } + + throw; + } + } + + public virtual async Task SetAsync(TCacheKey key, TCacheItem value, DistributedCacheEntryOptions options = null, bool? hideErrors = null, CancellationToken token = default) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + await Cache.SetAsync( + NormalizeKey(key.ToString()), + Serializer.Serialize(value), + options ?? DefaultCacheOptions, + CancellationTokenProvider.FallbackToProvider(token) + ); + } + catch (Exception ex) + { + if (hideErrors == true) + { + Logger.LogException(ex, LogLevel.Warning); + return; + } + + throw; + } + } + + public virtual void Refresh(TCacheKey key, bool? hideErrors = null) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + Cache.Refresh(NormalizeKey(key.ToString())); + } + catch (Exception ex) + { + if (hideErrors == true) + { + Logger.LogException(ex, LogLevel.Warning); + return; + } + + throw; + } + } + + public virtual async Task RefreshAsync(TCacheKey key, bool? hideErrors = null, CancellationToken token = default) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + await Cache.RefreshAsync(NormalizeKey(key.ToString()), CancellationTokenProvider.FallbackToProvider(token)); + } + catch (Exception ex) + { + if (hideErrors == true) + { + Logger.LogException(ex, LogLevel.Warning); + return; + } + + throw; + } + } + + public virtual void Remove(TCacheKey key, bool? hideErrors = null) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + Cache.Remove(NormalizeKey(key.ToString())); + } + catch (Exception ex) + { + if (hideErrors == true) + { + Logger.LogException(ex, LogLevel.Warning); + } + + throw; + } + } + + public virtual async Task RemoveAsync(TCacheKey key, bool? hideErrors = null, CancellationToken token = default) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + await Cache.RemoveAsync(NormalizeKey(key.ToString()), CancellationTokenProvider.FallbackToProvider(token)); + } + catch (Exception ex) + { + if (hideErrors == true) + { + Logger.LogException(ex, LogLevel.Warning); + return; + } + + throw; + } + } + + } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs index 23bd6f4cf2..82adeda726 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs @@ -148,4 +148,148 @@ namespace Volo.Abp.Caching CancellationToken token = default ); } + /// + /// Represents a distributed cache of type. + /// Uses a generic cache key type of type. + /// + /// The type of cache item being cached. + /// The type of cache key being used. + public interface IDistributedCache + where TCacheItem : class + { + /// + /// Gets a cache item with the given key. If no cache item is found for the given key then returns null. + /// + /// The key of cached item to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// The cache item, or null. + TCacheItem Get( + TCacheKey key, + bool? hideErrors = null + ); + + /// + /// Gets a cache item with the given key. If no cache item is found for the given key then returns null. + /// + /// The key of cached item to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// The for the task. + /// The cache item, or null. + Task GetAsync( + [NotNull] TCacheKey key, + bool? hideErrors = null, + CancellationToken token = default + ); + + /// + /// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item + /// provided by delegate and returns the provided cache item. + /// + /// The key of cached item to be retrieved from the cache. + /// The factory delegate is used to provide the cache item when no cache item is found for the given . + /// The cache options for the factory delegate. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// The cache item. + TCacheItem GetOrAdd( + TCacheKey key, + Func factory, + Func optionsFactory = null, + bool? hideErrors = null + ); + + /// + /// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item + /// provided by delegate and returns the provided cache item. + /// + /// The key of cached item to be retrieved from the cache. + /// The factory delegate is used to provide the cache item when no cache item is found for the given . + /// The cache options for the factory delegate. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// The for the task. + /// The cache item. + Task GetOrAddAsync( + [NotNull] TCacheKey key, + Func> factory, + Func optionsFactory = null, + bool? hideErrors = null, + CancellationToken token = default + ); + + /// + /// Sets the cache item value for the provided key. + /// + /// The key of cached item to be retrieved from the cache. + /// The cache item value to set in the cache. + /// The cache options for the value. + /// Indicates to throw or hide the exceptions for the distributed cache. + void Set( + TCacheKey key, + TCacheItem value, + DistributedCacheEntryOptions options = null, + bool? hideErrors = null + ); + + /// + /// Sets the cache item value for the provided key. + /// + /// The key of cached item to be retrieved from the cache. + /// The cache item value to set in the cache. + /// The cache options for the value. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// The for the task. + /// The indicating that the operation is asynchronous. + Task SetAsync( + [NotNull] TCacheKey key, + [NotNull] TCacheItem value, + [CanBeNull] DistributedCacheEntryOptions options = null, + bool? hideErrors = null, + CancellationToken token = default + ); + + /// + /// Refreshes the cache value of the given key, and resets its sliding expiration timeout. + /// + /// The key of cached item to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + void Refresh( + TCacheKey key, + bool? hideErrors = null + ); + + /// + /// Refreshes the cache value of the given key, and resets its sliding expiration timeout. + /// + /// The key of cached item to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// The for the task. + /// The indicating that the operation is asynchronous. + Task RefreshAsync( + TCacheKey key, + bool? hideErrors = null, + CancellationToken token = default + ); + + /// + /// Removes the cache item for given key from cache. + /// + /// The key of cached item to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + void Remove( + TCacheKey key, + bool? hideErrors = null + ); + + /// + /// Removes the cache item for given key from cache. + /// + /// The key of cached item to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// The for the task. + /// The indicating that the operation is asynchronous. + Task RemoveAsync( + TCacheKey key, + bool? hideErrors = null, + CancellationToken token = default + ); + } } diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs index fc6b443893..b802b3e297 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs @@ -122,5 +122,150 @@ namespace Volo.Abp.Caching cacheItem1.ShouldNotBeNull(); } + + [Fact] + public async Task Should_Set_Get_And_Remove_Cache_Items_With_Integer_Type_CacheKey() + { + var personCache = GetRequiredService>(); + + var cacheKey = 42; + const string personName = "john nash"; + + //Get (not exists yet) + var cacheItem = await personCache.GetAsync(cacheKey); + cacheItem.ShouldBeNull(); + + //Set + cacheItem = new PersonCacheItem(personName); + await personCache.SetAsync(cacheKey, cacheItem); + + //Get (it should be available now + cacheItem = await personCache.GetAsync(cacheKey); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + + //Remove + await personCache.RemoveAsync(cacheKey); + + //Get (not exists since removed) + cacheItem = await personCache.GetAsync(cacheKey); + cacheItem.ShouldBeNull(); + } + + [Fact] + public async Task GetOrAddAsync_With_Integer_Type_CacheKey() + { + var personCache = GetRequiredService>(); + + var cacheKey = 42; + const string personName = "john nash"; + + //Will execute the factory method to create the cache item + + bool factoryExecuted = false; + + var cacheItem = await personCache.GetOrAddAsync(cacheKey, + async () => + { + factoryExecuted = true; + return new PersonCacheItem(personName); + }); + + factoryExecuted.ShouldBeTrue(); + cacheItem.Name.ShouldBe(personName); + + //This time, it will not execute the factory + + factoryExecuted = false; + + cacheItem = await personCache.GetOrAddAsync(cacheKey, + async () => + { + factoryExecuted = true; + return new PersonCacheItem(personName); + }); + + factoryExecuted.ShouldBeFalse(); + cacheItem.Name.ShouldBe(personName); + } + + [Fact] + public async Task SameClassName_But_DiffNamespace_Should_Not_Use_Same_Cache_With_Integer_CacheKey() + { + var personCache = GetRequiredService>(); + var otherPersonCache = GetRequiredService>(); + + + var cacheKey = 42; + const string personName = "john nash"; + + //Get (not exists yet) + var cacheItem = await personCache.GetAsync(cacheKey); + cacheItem.ShouldBeNull(); + + var cacheItem1 = await otherPersonCache.GetAsync(cacheKey); + cacheItem1.ShouldBeNull(); + + //Set + cacheItem = new PersonCacheItem(personName); + await personCache.SetAsync(cacheKey, cacheItem); + + //Get (it should be available now, but otherPersonCache not exists now. + cacheItem = await personCache.GetAsync(cacheKey); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + + cacheItem1 = await otherPersonCache.GetAsync(cacheKey); + cacheItem1.ShouldBeNull(); + + //set other person cache + cacheItem1 = new Sail.Testing.Caching.PersonCacheItem(personName); + await otherPersonCache.SetAsync(cacheKey, cacheItem1); + + cacheItem1 = await otherPersonCache.GetAsync(cacheKey); + cacheItem1.ShouldNotBeNull(); + cacheItem1.Name.ShouldBe(personName); + + //Remove + await personCache.RemoveAsync(cacheKey); + + + //Get (not exists since removed) + cacheItem = await personCache.GetAsync(cacheKey); + cacheItem.ShouldBeNull(); + + cacheItem1 = await otherPersonCache.GetAsync(cacheKey); + cacheItem1.ShouldNotBeNull(); + + } + + [Fact] + public async Task Should_Set_Get_And_Remove_Cache_Items_With_Object_Type_CacheKey() + { + var personCache = GetRequiredService>(); + + var cacheKey = new DummyObjectAsCacheKey { DummyData = "DummyData", DummyInt = 42 }; + const string personName = "john nash"; + + //Get (not exists yet) + var cacheItem = await personCache.GetAsync(cacheKey); + cacheItem.ShouldBeNull(); + + //Set + cacheItem = new PersonCacheItem(personName); + await personCache.SetAsync(cacheKey, cacheItem); + + //Get (it should be available now + cacheItem = await personCache.GetAsync(cacheKey); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + + //Remove + await personCache.RemoveAsync(cacheKey); + + //Get (not exists since removed) + cacheItem = await personCache.GetAsync(cacheKey); + cacheItem.ShouldBeNull(); + } } } \ No newline at end of file diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/PersonCacheItem.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/PersonCacheItem.cs index b6c6f1dd0a..6b3d9c36df 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/PersonCacheItem.cs +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/PersonCacheItem.cs @@ -17,7 +17,14 @@ namespace Volo.Abp.Caching Name = name; } } + + public class DummyObjectAsCacheKey + { + public string DummyData { get; set; } + public int DummyInt { get; set; } + } } + namespace Sail.Testing.Caching { [Serializable]