diff --git a/Directory.Packages.props b/Directory.Packages.props
index 594c861bc6..3759f383af 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 74a41a5507..7163d3ce6c 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 769cc75a8d..de5f60ee4e 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 class AbpCachingModule : AbpModule
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 0000000000..a679802703
--- /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 0000000000..b94d1a2ea2
--- /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 0000000000..65965ecf87
--- /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 0000000000..039a5e3142
--- /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 0000000000..4fb61a6e75
--- /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 b45cded03c..21f39bdc77 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 class AbpCachingTestModule : AbpModule
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 0000000000..04d66903f7
--- /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");
+ }
+}