From 1b11f2de8f6f4059a81c3e7dbf00467485e3edc2 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 23 Sep 2024 10:52:35 +0800 Subject: [PATCH] Add `AbpHybridCache`. --- Directory.Packages.props | 1 + .../Volo.Abp.Caching/Volo.Abp.Caching.csproj | 1 + .../Volo/Abp/Caching/AbpCachingModule.cs | 7 + .../Volo/Abp/Caching/Hybrid/AbpHybridCache.cs | 459 ++++++++++++++++ .../Hybrid/AbpHybridCacheJsonSerializer.cs | 27 + .../AbpHybridCacheJsonSerializerFactory.cs | 27 + .../Caching/Hybrid/AbpHybridCacheOptions.cs | 51 ++ .../Volo/Abp/Caching/Hybrid/IHybridCache.cs | 93 ++++ .../Volo/Abp/Caching/AbpCachingTestModule.cs | 25 + .../Volo/Abp/Caching/HybridCache_Tests.cs | 519 ++++++++++++++++++ 10 files changed, 1210 insertions(+) create mode 100644 framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs create mode 100644 framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheJsonSerializer.cs create mode 100644 framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheJsonSerializerFactory.cs create mode 100644 framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheOptions.cs create mode 100644 framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/IHybridCache.cs create mode 100644 framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/HybridCache_Tests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 594c861bc67..3759f383af9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -80,6 +80,7 @@ + diff --git a/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj b/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj index 74a41a5507b..7163d3ce6c7 100644 --- a/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj +++ b/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj @@ -17,6 +17,7 @@ + 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 769cc75a8db..de5f60ee4e3 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs @@ -1,5 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using System; +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Caching.Hybrid; using Volo.Abp.Json; using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; @@ -25,6 +28,10 @@ public override void ConfigureServices(ServiceConfigurationContext context) context.Services.AddSingleton(typeof(IDistributedCache<>), typeof(DistributedCache<>)); context.Services.AddSingleton(typeof(IDistributedCache<,>), typeof(DistributedCache<,>)); + context.Services.AddHybridCache().AddSerializerFactory(); + context.Services.AddSingleton(typeof(IHybridCache<>), typeof(AbpHybridCache<>)); + context.Services.AddSingleton(typeof(IHybridCache<,>), typeof(AbpHybridCache<,>)); + context.Services.Configure(cacheOptions => { cacheOptions.GlobalCacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes(20); diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs new file mode 100644 index 00000000000..a6798027035 --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs @@ -0,0 +1,459 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.ExceptionHandling; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Threading; +using Volo.Abp.Uow; + +namespace Volo.Abp.Caching.Hybrid; + +/// +/// Represents a hybrid cache of items. +/// +/// The type of the cache item being cached. +public class AbpHybridCache : IHybridCache + where TCacheItem : class +{ + public IHybridCache InternalCache { get; } + + public AbpHybridCache(IHybridCache internalCache) + { + InternalCache = internalCache; + } + + public virtual async Task GetOrCreateAsync(string key, Func> factory, Func? optionsFactory = null, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + return await InternalCache.GetOrCreateAsync(key, factory, optionsFactory, hideErrors, considerUow, token); + } + + public virtual async Task SetAsync(string key, TCacheItem value, HybridCacheEntryOptions? options = null, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + await InternalCache.SetAsync(key, value, options, hideErrors, considerUow, token); + } + + public virtual async Task RemoveAsync(string key, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + await InternalCache.RemoveAsync(key, hideErrors, considerUow, token); + } + + public virtual async Task RemoveManyAsync(IEnumerable keys, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + await InternalCache.RemoveManyAsync(keys, hideErrors, considerUow, token); + } +} + +/// +/// Represents a hybrid cache of items. +/// Uses as the key type. +/// +/// The type of cache item being cached. +/// The type of cache key being used. +public class AbpHybridCache : IHybridCache + where TCacheItem : class + where TCacheKey : notnull +{ + public const string UowCacheName = "AbpHybridCache"; + + public ILogger> Logger { get; set; } + + protected string CacheName { get; set; } = default!; + + protected bool IgnoreMultiTenancy { get; set; } + + protected IServiceProvider ServiceProvider { get; } + + protected HybridCache HybridCache { get; } + + protected IDistributedCache DistributedCacheCache { get; } + + protected ICancellationTokenProvider CancellationTokenProvider { get; } + + protected IDistributedCacheKeyNormalizer KeyNormalizer { get; } + + protected IServiceScopeFactory ServiceScopeFactory { get; } + + protected IUnitOfWorkManager UnitOfWorkManager { get; } + + protected SemaphoreSlim SyncSemaphore { get; } + + protected HybridCacheEntryOptions DefaultCacheOptions = default!; + + protected AbpHybridCacheOptions DistributedCacheOption { get; } + + public AbpHybridCache( + IServiceProvider serviceProvider, + IOptions distributedCacheOption, + HybridCache hybridCache, + IDistributedCache distributedCache, + ICancellationTokenProvider cancellationTokenProvider, + IDistributedCacheSerializer serializer, + IDistributedCacheKeyNormalizer keyNormalizer, + IServiceScopeFactory serviceScopeFactory, + IUnitOfWorkManager unitOfWorkManager) + { + ServiceProvider = serviceProvider; + DistributedCacheOption = distributedCacheOption.Value; + HybridCache = hybridCache; + DistributedCacheCache = distributedCache; + CancellationTokenProvider = cancellationTokenProvider; + Logger = NullLogger>.Instance; + KeyNormalizer = keyNormalizer; + ServiceScopeFactory = serviceScopeFactory; + UnitOfWorkManager = unitOfWorkManager; + + SyncSemaphore = new SemaphoreSlim(1, 1); + + SetDefaultOptions(); + } + + protected virtual string NormalizeKey(TCacheKey key) + { + return KeyNormalizer.NormalizeKey( + new DistributedCacheKeyNormalizeArgs( + key.ToString()!, + CacheName, + IgnoreMultiTenancy + ) + ); + } + + protected virtual HybridCacheEntryOptions GetDefaultCacheEntryOptions() + { + foreach (var configure in DistributedCacheOption.CacheConfigurators) + { + var options = configure.Invoke(CacheName); + if (options != null) + { + return options; + } + } + + return DistributedCacheOption.GlobalHybridCacheEntryOptions; + } + + protected virtual void SetDefaultOptions() + { + CacheName = CacheNameAttribute.GetCacheName(typeof(TCacheItem)); + + //IgnoreMultiTenancy + IgnoreMultiTenancy = typeof(TCacheItem).IsDefined(typeof(IgnoreMultiTenancyAttribute), true); + + //Configure default cache entry options + DefaultCacheOptions = GetDefaultCacheEntryOptions(); + } + + /// + /// Gets or Creates 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. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// The for the task. + /// The cache item. + public virtual async Task GetOrCreateAsync( + TCacheKey key, + Func> factory, + Func? optionsFactory = null, + bool? hideErrors = null, + bool considerUow = false, + CancellationToken token = default) + { + token = CancellationTokenProvider.FallbackToProvider(token); + hideErrors = hideErrors ?? DistributedCacheOption.HideErrors; + + TCacheItem? value = null; + + if (!considerUow) + { + try + { + value = await HybridCache.GetOrCreateAsync( + key: NormalizeKey(key), + factory: async cancel => await factory(), + options: optionsFactory?.Invoke(), + tags: null, + cancellationToken: token); + } + catch (Exception ex) + { + if (hideErrors == true) + { + await HandleExceptionAsync(ex); + return null; + } + + throw; + } + + return value; + } + + try + { + using (await SyncSemaphore.LockAsync(token)) + { + if (ShouldConsiderUow(considerUow)) + { + value = GetUnitOfWorkCache().GetOrDefault(key)?.GetUnRemovedValueOrNull(); + if (value != null) + { + return value; + } + } + + var bytes = await DistributedCacheCache.GetAsync(NormalizeKey(key), token); + if (bytes != null) + { + return ResolveSerializer().Deserialize(new ReadOnlySequence(bytes, 0, bytes.Length));; + } + + value = await factory(); + + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + if (uowCache.TryGetValue(key, out var item)) + { + item.SetValue(value); + } + else + { + uowCache.Add(key, new UnitOfWorkCacheItem(value)); + } + } + + await SetAsync(key, value, optionsFactory?.Invoke(), hideErrors, considerUow, token); + } + } + catch (Exception ex) + { + if (hideErrors == true) + { + await HandleExceptionAsync(ex); + return null; + } + + throw; + } + + return value; + } + + /// + /// 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. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// The for the task. + /// The indicating that the operation is asynchronous. + public virtual async Task SetAsync( + TCacheKey key, + TCacheItem value, + HybridCacheEntryOptions? options = null, + bool? hideErrors = null, + bool considerUow = false, + CancellationToken token = default) + { + async Task SetRealCache() + { + token = CancellationTokenProvider.FallbackToProvider(token); + hideErrors = hideErrors ?? DistributedCacheOption.HideErrors; + + try + { + await HybridCache.SetAsync( + key: NormalizeKey(key), + value: value, + options: options ?? DefaultCacheOptions, + tags: null, + cancellationToken: token + ); + } + catch (Exception ex) + { + if (hideErrors == true) + { + await HandleExceptionAsync(ex); + return; + } + + throw; + } + } + + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + if (uowCache.TryGetValue(key, out _)) + { + uowCache[key].SetValue(value); + } + else + { + uowCache.Add(key, new UnitOfWorkCacheItem(value)); + } + + UnitOfWorkManager.Current?.OnCompleted(SetRealCache); + } + else + { + await SetRealCache(); + } + } + + /// + /// 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. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// The for the task. + /// The indicating that the operation is asynchronous. + public virtual async Task RemoveAsync( + TCacheKey key, + bool? hideErrors = null, + bool considerUow = false, + CancellationToken token = default) + { + await RemoveManyAsync(new[] { key }, hideErrors, considerUow, token); + } + + /// + /// Removes the cache items for given keys from cache. + /// + /// The keys of cached items to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// The for the task. + /// The indicating that the operation is asynchronous. + public async Task RemoveManyAsync( + IEnumerable keys, + bool? hideErrors = null, + bool considerUow = false, + CancellationToken token = default) + { + var keyArray = keys.ToArray(); + + async Task RemoveRealCache() + { + hideErrors = hideErrors ?? DistributedCacheOption.HideErrors; + + try + { + await HybridCache.RemoveAsync( + keyArray.Select(NormalizeKey), token); + } + catch (Exception ex) + { + if (hideErrors == true) + { + await HandleExceptionAsync(ex); + return; + } + + throw; + } + } + + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + + foreach (var key in keyArray) + { + if (uowCache.TryGetValue(key, out _)) + { + uowCache[key].RemoveValue(); + } + } + + UnitOfWorkManager.Current?.OnCompleted(RemoveRealCache); + } + else + { + await RemoveRealCache(); + } + } + + protected virtual async Task HandleExceptionAsync(Exception ex) + { + Logger.LogException(ex, LogLevel.Warning); + + using (var scope = ServiceScopeFactory.CreateScope()) + { + await scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning)); + } + } + + protected virtual bool ShouldConsiderUow(bool considerUow) + { + return considerUow && UnitOfWorkManager.Current != null; + } + + protected virtual string GetUnitOfWorkCacheKey() + { + return UowCacheName + CacheName; + } + + protected virtual Dictionary> GetUnitOfWorkCache() + { + if (UnitOfWorkManager.Current == null) + { + throw new AbpException($"There is no active UOW."); + } + + return UnitOfWorkManager.Current.GetOrAddItem(GetUnitOfWorkCacheKey(), + key => new Dictionary>()); + } + + private readonly ConcurrentDictionary _serializersCache = new(); + + protected virtual IHybridCacheSerializer ResolveSerializer() + { + if (_serializersCache.TryGetValue(typeof(TCacheItem), out var serializer)) + { + return serializer.As>(); + } + + serializer = ServiceProvider.GetService>(); + if (serializer is null) + { + var factories = ServiceProvider.GetServices().ToArray(); + Array.Reverse(factories); + foreach (var factory in factories) + { + if (factory.TryCreateSerializer(out var current)) + { + serializer = current; + break; + } + } + } + + if (serializer is null) + { + throw new InvalidOperationException($"No {nameof(IHybridCacheSerializer)} configured for type '{typeof(TCacheItem).Name}'"); + } + + return serializer.As>(); + } +} diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheJsonSerializer.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheJsonSerializer.cs new file mode 100644 index 00000000000..b94d1a2ea2c --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheJsonSerializer.cs @@ -0,0 +1,27 @@ +using System.Buffers; +using System.Text.Json; +using Microsoft.Extensions.Caching.Hybrid; + +namespace Volo.Abp.Caching.Hybrid; + +public class AbpHybridCacheJsonSerializer : IHybridCacheSerializer +{ + protected JsonSerializerOptions JsonSerializerOptions { get; } + + public AbpHybridCacheJsonSerializer(JsonSerializerOptions jsonSerializerOptions) + { + JsonSerializerOptions = jsonSerializerOptions; + } + + public virtual T Deserialize(ReadOnlySequence source) + { + var reader = new Utf8JsonReader(source); + return JsonSerializer.Deserialize(ref reader, JsonSerializerOptions)!; + } + + public virtual void Serialize(T value, IBufferWriter target) + { + using var writer = new Utf8JsonWriter(target); + JsonSerializer.Serialize(writer, value, JsonSerializerOptions); + } +} diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheJsonSerializerFactory.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheJsonSerializerFactory.cs new file mode 100644 index 00000000000..65965ecf870 --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheJsonSerializerFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Options; +using Volo.Abp.Json.SystemTextJson; + +namespace Volo.Abp.Caching.Hybrid; + +public class AbpHybridCacheJsonSerializerFactory : IHybridCacheSerializerFactory +{ + protected IOptions Options { get; } + + public AbpHybridCacheJsonSerializerFactory(IOptions options) + { + Options = options; + } + + public bool TryCreateSerializer(out IHybridCacheSerializer? serializer) + { + if (typeof(T) == typeof(string) || typeof(T) == typeof(byte[])) + { + serializer = null; + return false; + } + + serializer = new AbpHybridCacheJsonSerializer(Options.Value.JsonSerializerOptions); + return true; + } +} diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheOptions.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheOptions.cs new file mode 100644 index 00000000000..039a5e31429 --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCacheOptions.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Hybrid; + +namespace Volo.Abp.Caching.Hybrid; + +public class AbpHybridCacheOptions +{ + /// + /// Throw or hide exceptions for the distributed cache. + /// + public bool HideErrors { get; set; } = true; + + /// + /// Cache key prefix. + /// + public string KeyPrefix { get; set; } + + /// + /// Global Cache entry options. + /// + public HybridCacheEntryOptions GlobalHybridCacheEntryOptions { get; set; } + + /// + /// List of all cache configurators. + /// (func argument:Name of cache) + /// + public List> CacheConfigurators { get; set; } //TODO: use a configurator interface instead? + + public AbpHybridCacheOptions() + { + CacheConfigurators = new List>(); + GlobalHybridCacheEntryOptions = new HybridCacheEntryOptions(); + KeyPrefix = ""; + } + + public void ConfigureCache(HybridCacheEntryOptions? options) + { + ConfigureCache(typeof(TCacheItem), options); + } + + public void ConfigureCache(Type cacheItemType, HybridCacheEntryOptions? options) + { + ConfigureCache(CacheNameAttribute.GetCacheName(cacheItemType), options); + } + + public void ConfigureCache(string cacheName, HybridCacheEntryOptions? options) + { + CacheConfigurators.Add(name => cacheName != name ? null : options); + } +} diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/IHybridCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/IHybridCache.cs new file mode 100644 index 00000000000..4fb61a6e750 --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/IHybridCache.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Hybrid; + +namespace Volo.Abp.Caching.Hybrid; + +/// +/// Represents a hybrid cache of items. +/// +/// The type of the cache item being cached. +public interface IHybridCache : IHybridCache + where TCacheItem : class +{ + IHybridCache InternalCache { get; } +} + +/// +/// Represents a hybrid cache of items. +/// Uses as the key type. +/// +/// The type of cache item being cached. +/// The type of cache key being used. +public interface IHybridCache + where TCacheItem : class +{ + /// + /// Gets or Creates 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. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// The for the task. + /// The cache item. + Task GetOrCreateAsync( + [NotNull]TCacheKey key, + Func> factory, + Func? optionsFactory = null, + bool? hideErrors = null, + bool considerUow = false, + 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. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// The for the task. + /// The indicating that the operation is asynchronous. + Task SetAsync( + [NotNull]TCacheKey key, + TCacheItem value, + HybridCacheEntryOptions? options = null, + bool? hideErrors = null, + bool considerUow = false, + 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. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// The for the task. + /// The indicating that the operation is asynchronous. + Task RemoveAsync( + [NotNull]TCacheKey key, + bool? hideErrors = null, + bool considerUow = false, + CancellationToken token = default); + + /// + /// Removes the cache items for given keys from cache. + /// + /// The keys of cached items to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// The for the task. + /// The indicating that the operation is asynchronous. + Task RemoveManyAsync( + IEnumerable keys, + bool? hideErrors = null, + bool considerUow = false, + CancellationToken token = default); +} diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs index b45cded03cc..21f39bdc770 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.Caching.Distributed; using System; +using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Caching.Hybrid; using Volo.Abp.Modularity; namespace Volo.Abp.Caching; @@ -29,6 +31,29 @@ public override void ConfigureServices(ServiceConfigurationContext context) option.GlobalCacheEntryOptions.SetSlidingExpiration(TimeSpan.FromMinutes(20)); }); + Configure(option => + { + option.CacheConfigurators.Add(cacheName => + { + if (cacheName == CacheNameAttribute.GetCacheName(typeof(Sail.Testing.Caching.PersonCacheItem))) + { + return new HybridCacheEntryOptions() + { + Expiration = TimeSpan.FromMinutes(10), + LocalCacheExpiration = TimeSpan.FromMinutes(5) + }; + } + + return null; + }); + + option.GlobalHybridCacheEntryOptions = new HybridCacheEntryOptions() + { + Expiration = TimeSpan.FromMinutes(20), + LocalCacheExpiration = TimeSpan.FromMinutes(10) + }; + }); + context.Services.Replace(ServiceDescriptor.Singleton()); } } diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/HybridCache_Tests.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/HybridCache_Tests.cs new file mode 100644 index 00000000000..04d66903f78 --- /dev/null +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/HybridCache_Tests.cs @@ -0,0 +1,519 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Caching.Hybrid; +using Volo.Abp.Testing; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Caching; + +public class HybridCache_Tests : AbpIntegratedTest +{ + [Fact] + public async Task Should_GetOrCreate_Set_And_Remove_Cache_Items() + { + var personCache = GetRequiredService>(); + + var cacheKey = Guid.NewGuid().ToString(); + + //GetOrCreateAsync + var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("john nash"))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe("john nash"); + + //SetAsync + await personCache.SetAsync(cacheKey, new PersonCacheItem("baris")); + + //GetOrCreateAsync + cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("john nash"))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe("baris"); + + //RemoveAsync + await personCache.RemoveAsync(cacheKey); + + //GetOrCreateAsync + cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("lucas"))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe("lucas"); + } + + [Fact] + public async Task GetOrCreateAsync() + { + var personCache = GetRequiredService>(); + + var cacheKey = Guid.NewGuid().ToString(); + const string personName = "john nash"; + + //Will execute the factory method to create the cache item + + bool factoryExecuted = false; + + var cacheItem = await personCache.GetOrCreateAsync(cacheKey, + () => + { + factoryExecuted = true; + return Task.FromResult(new PersonCacheItem(personName)); + }); + + factoryExecuted.ShouldBeTrue(); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + + //This time, it will not execute the factory + + factoryExecuted = false; + + cacheItem = await personCache.GetOrCreateAsync(cacheKey, + () => + { + factoryExecuted = true; + return Task.FromResult(new PersonCacheItem(personName)); + }); + + factoryExecuted.ShouldBeFalse(); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + } + + [Fact] + public async Task SameClassName_But_DiffNamespace_Should_Not_Use_Same_Cache() + { + var personCache = GetRequiredService>(); + var otherPersonCache = GetRequiredService>(); + + var cacheKey = Guid.NewGuid().ToString(); + const string personName = "john nash"; + + var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + var cacheItem1 = await otherPersonCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new Sail.Testing.Caching.PersonCacheItem(personName))); + cacheItem1.ShouldNotBeNull(); + cacheItem1.Name.ShouldBe(personName); + + await personCache.RemoveAsync(cacheKey); + + cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName + "1"))); + cacheItem1 = await otherPersonCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new Sail.Testing.Caching.PersonCacheItem(personName + "1"))); + + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName + "1"); + + cacheItem1.ShouldNotBeNull(); + cacheItem1.Name.ShouldBe(personName); + } + + [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"; + + //GetOrCreateAsync + var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + + //SetAsync + await personCache.SetAsync(cacheKey, new PersonCacheItem("baris")); + + //GetOrCreateAsync + cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe("baris"); + + //RemoveAsync + await personCache.RemoveAsync(cacheKey); + + //GetOrCreateAsync + cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("lucas"))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe("lucas"); + } + + [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.GetOrCreateAsync(cacheKey, + () => + { + factoryExecuted = true; + return Task.FromResult(new PersonCacheItem(personName)); + }); + + factoryExecuted.ShouldBeTrue(); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + + //This time, it will not execute the factory + + factoryExecuted = false; + + cacheItem = await personCache.GetOrCreateAsync(cacheKey, + () => + { + factoryExecuted = true; + return Task.FromResult(new PersonCacheItem(personName)); + }); + + factoryExecuted.ShouldBeFalse(); + cacheItem.ShouldNotBeNull(); + 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"; + + var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + var cacheItem1 = await otherPersonCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new Sail.Testing.Caching.PersonCacheItem(personName))); + cacheItem1.ShouldNotBeNull(); + cacheItem1.Name.ShouldBe(personName); + + await personCache.RemoveAsync(cacheKey); + + cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName + "1"))); + cacheItem1 = await otherPersonCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new Sail.Testing.Caching.PersonCacheItem(personName + "1"))); + + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName + "1"); + + cacheItem1.ShouldNotBeNull(); + cacheItem1.Name.ShouldBe(personName); + } + + [Fact] + public async Task Should_Set_Get_And_Remove_Cache_Items_With_Object_Type_CacheKey() + { + var personCache = GetRequiredService>(); + + var cacheKey = new ComplexObjectAsCacheKey { Name = "DummyData", Age = 42 }; + const string personName = "john nash"; + + //GetOrCreateAsync + var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe(personName); + + //SetAsync + await personCache.SetAsync(cacheKey, new PersonCacheItem("baris")); + + //GetOrCreateAsync + cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe("baris"); + + //RemoveAsync + await personCache.RemoveAsync(cacheKey); + + //GetOrCreateAsync + cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("lucas"))); + cacheItem.ShouldNotBeNull(); + cacheItem.Name.ShouldBe("lucas"); + } + + [Fact] + public async Task Should_Set_Get_And_Remove_Cache_Items_For_Same_Object_Type_With_Different_CacheKeys() + { + var personCache = GetRequiredService>(); + + var cache1Key = new ComplexObjectAsCacheKey { Name = "John", Age = 42 }; + var cache2Key = new ComplexObjectAsCacheKey { Name = "Jenny", Age = 24 }; + const string personName = "john nash"; + + //GetOrCreateAsync + var cacheItem1 = await personCache.GetOrCreateAsync(cache1Key, () => Task.FromResult(new PersonCacheItem(personName))); + var cacheItem2 = await personCache.GetOrCreateAsync(cache2Key, () => Task.FromResult(new PersonCacheItem(personName))); + cacheItem1.ShouldNotBeNull(); + cacheItem1.Name.ShouldBe(personName); + cacheItem2.ShouldNotBeNull(); + cacheItem2.Name.ShouldBe(personName); + + //SetAsync + cacheItem1 = new PersonCacheItem("baris"); + cacheItem2 = new PersonCacheItem("jack"); + await personCache.SetAsync(cache1Key, cacheItem1); + await personCache.SetAsync(cache2Key, cacheItem2); + + //GetOrCreateAsync + cacheItem1 = await personCache.GetOrCreateAsync(cache1Key, () => Task.FromResult(new PersonCacheItem(personName))); + cacheItem2 = await personCache.GetOrCreateAsync(cache2Key, () => Task.FromResult(new PersonCacheItem(personName))); + cacheItem1.ShouldNotBeNull(); + cacheItem1.Name.ShouldBe("baris"); + cacheItem2.ShouldNotBeNull(); + cacheItem2.Name.ShouldBe("jack"); + + //Remove + await personCache.RemoveAsync(cache1Key); + await personCache.RemoveAsync(cache2Key); + + //Get (not exists since removed) + cacheItem1 = await personCache.GetOrCreateAsync(cache1Key, () => Task.FromResult(new PersonCacheItem("lucas"))); + cacheItem2 = await personCache.GetOrCreateAsync(cache2Key, () => Task.FromResult(new PersonCacheItem("peter"))); + cacheItem1.ShouldNotBeNull(); + cacheItem1.Name.ShouldBe("lucas"); + cacheItem2.ShouldNotBeNull(); + cacheItem2.Name.ShouldBe("peter"); + } + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_GetOrCreateAsync() + { + const string key = "testkey"; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + await personCache.SetAsync(key, new PersonCacheItem("lucas"), considerUow: true); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john2")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("lucas"); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john3"); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("lucas"); + }); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_GetOrCreateAsync() + { + const string key = "testkey"; + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + using (var uow = GetRequiredService().Begin()) + { + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john2")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + await personCache.SetAsync(key, new PersonCacheItem("john3"), considerUow: true); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john3"); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john5")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + } + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john6")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + } + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_SetAsync() + { + const string key = "testkey"; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); + + var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john2")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john3"); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + }); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_SetAsync() + { + const string key = "testkey"; + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + using (var uow = GetRequiredService().Begin()) + { + await personCache.SetAsync(key, new PersonCacheItem("john2"), considerUow: true); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john2"); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + } + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john5")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + } + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_RemoveAsync() + { + const string key = "testkey"; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); + + var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john2")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + await personCache.RemoveAsync(key, considerUow: true); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john3"); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john4"); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john5")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john3"); + }); + + await uow.CompleteAsync(); + } + } + + public async Task Cache_Should_Rollback_With_Uow_For_RemoveAsync() + { + const string key = "testkey"; + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + using (var uow = GetRequiredService().Begin()) + { + await personCache.SetAsync(key, new PersonCacheItem("john2"), considerUow: true); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john2"); + + await personCache.RemoveAsync(key, considerUow: true); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + } + + cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + } + + [Fact] + public async Task Should_Remove_Multiple_Items_Async() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + var testkey3 = new[] { testkey, testkey2 }; + + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetOrCreateAsync(testkey, () => Task.FromResult(new PersonCacheItem("john"))); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetOrCreateAsync(testkey2, () => Task.FromResult(new PersonCacheItem("jack"))); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("jack"); + + await personCache.RemoveManyAsync(testkey3); + + cacheValue = await personCache.GetOrCreateAsync(testkey, () => Task.FromResult(new PersonCacheItem("john2"))); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john2"); + + cacheValue = await personCache.GetOrCreateAsync(testkey2, () => Task.FromResult(new PersonCacheItem("jack2"))); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("jack2"); + } + + [Fact] + public async Task Should_Get_Same_Cache_Set_When_Resolve_With_Or_Without_Key() + { + var cache1 = GetRequiredService>(); + var cache2 = GetRequiredService>(); + + cache1.InternalCache.ShouldBe(cache2); + + await cache1.SetAsync("john", new PersonCacheItem("john")); + + var item1 = await cache1.GetOrCreateAsync("john", () => Task.FromResult(new PersonCacheItem("john2"))); + item1.ShouldNotBeNull(); + item1.Name.ShouldBe("john"); + + var item2 = await cache1.GetOrCreateAsync("john", () => Task.FromResult(new PersonCacheItem("john3"))); + item2.ShouldNotBeNull(); + item2.Name.ShouldBe("john"); + } +}