diff --git a/docs/en/Caching.md b/docs/en/Caching.md index 042f13ffe0..7dbf02d875 100644 --- a/docs/en/Caching.md +++ b/docs/en/Caching.md @@ -242,12 +242,15 @@ In addition, all of the `IDistributedCache` (and `IDistributedCache< ## Batch Operations -ABP's distributed cache interfaces provide methods to perform batch get/set methods those improves the performance when you want to get or set multiple cache items in a single method call. +ABP's distributed cache interfaces provide methods to perform batch methods those improves the performance when you want to batch operation multiple cache items in a single method call. * `SetManyAsync` and `SetMany` methods can be used to set multiple values to the cache. * `GetManyAsync` and `GetMany` methods can be used to retrieve multiple values from the cache. +* `GetOrAddManyAsync` and `GetOrAddMany` methods can be used to retrieve multiple values and set missing values from the cache +* `RefreshManyAsync` and `RefreshMany` methods can be used to resets the sliding expiration timeout of multiple values from the cache +* `RemoveManyAsync` and `RemoveMany` methods can be used to remove multiple values from the cache -> These are not standard methods of the ASP.NET Core caching. So, some providers may not support them. They are supported by the [ABP Redis Cache integration package](Redis-Cache.md). If the provider doesn't support, it fallbacks to `SetAsync` and `GetAsync` methods (called once for each item). +> These are not standard methods of the ASP.NET Core caching. So, some providers may not support them. They are supported by the [ABP Redis Cache integration package](Redis-Cache.md). If the provider doesn't support, it fallbacks to `SetAsync` and `GetAsync` ... methods (called once for each item). ## Advanced Topics diff --git a/docs/zh-Hans/Caching.md b/docs/zh-Hans/Caching.md index 0557ddcb4d..a75199ce47 100644 --- a/docs/zh-Hans/Caching.md +++ b/docs/zh-Hans/Caching.md @@ -192,6 +192,18 @@ public class BookService : ITransientDependency } ```` +## 批量操作 + +ABP的分布式缓存接口定义了以下批量操作方法,当你需要在一个方法中调用多次缓存操作时,这些方法可以提高性能 + +* `SetManyAsync` 和 `SetMany` 方法可以用来设置多个值. +* `GetManyAsync` 和 `GetMany` 方法可以用来从缓存中获取多个值. +* `GetOrAddManyAsync` 和 `GetOrAddMany` 方法可以用来从缓存中获取并添加缺少的值. +* `RefreshManyAsync` 和 `RefreshMany` 方法可以来用重置多个值的滚动过期时间. +* `RemoveManyAsync` 和 `RemoveMany` 方法呆以用来删除多个值. + +> 这些不是标准的ASP.NET Core缓存方法, 所以某些提供程序可能不支持. [ABP Redis集成包](Redis-Cache.md)实现了它们. 如果提供程序不支持,会回退到 `SetAsync` 和 `GetAsync` ... 方法(循环调用). + ### DistributedCacheOptions TODO \ No newline at end of file diff --git a/docs/zh-Hans/Redis-Cache.md b/docs/zh-Hans/Redis-Cache.md new file mode 100644 index 0000000000..cb82a640d5 --- /dev/null +++ b/docs/zh-Hans/Redis-Cache.md @@ -0,0 +1 @@ +TODO... \ No newline at end of file diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs index c3cb34f30c..fc45ccd05d 100644 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs @@ -45,19 +45,26 @@ namespace Volo.Abp.Caching.StackExchangeRedis MapMetadataMethod = type.GetMethod("MapMetadata", BindingFlags.Instance | BindingFlags.NonPublic); - GetAbsoluteExpirationMethod = type.GetMethod("GetAbsoluteExpiration", BindingFlags.Static | BindingFlags.NonPublic); + GetAbsoluteExpirationMethod = + type.GetMethod("GetAbsoluteExpiration", BindingFlags.Static | BindingFlags.NonPublic); - GetExpirationInSecondsMethod = type.GetMethod("GetExpirationInSeconds", BindingFlags.Static | BindingFlags.NonPublic); + GetExpirationInSecondsMethod = + type.GetMethod("GetExpirationInSeconds", BindingFlags.Static | BindingFlags.NonPublic); - SetScript = type.GetField("SetScript", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString(); + SetScript = type.GetField("SetScript", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) + .ToString(); - AbsoluteExpirationKey = type.GetField("AbsoluteExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString(); + AbsoluteExpirationKey = type.GetField("AbsoluteExpirationKey", BindingFlags.Static | BindingFlags.NonPublic) + ?.GetValue(null).ToString(); - SlidingExpirationKey = type.GetField("SlidingExpirationKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString(); + SlidingExpirationKey = type.GetField("SlidingExpirationKey", BindingFlags.Static | BindingFlags.NonPublic) + ?.GetValue(null).ToString(); - DataKey = type.GetField("DataKey", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).ToString(); + DataKey = type.GetField("DataKey", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null).ToString(); - NotPresent = type.GetField("NotPresent", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null).To(); + // ReSharper disable once PossibleNullReferenceException + NotPresent = type.GetField("NotPresent", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null) + .To(); } public AbpRedisCache(IOptions optionsAccessor) @@ -124,6 +131,42 @@ namespace Volo.Abp.Caching.StackExchangeRedis await Task.WhenAll(PipelineSetMany(items, options)); } + public void RefreshMany( + IEnumerable keys) + { + keys = Check.NotNull(keys, nameof(keys)); + + GetAndRefreshMany(keys, false); + } + + public async Task RefreshManyAsync( + IEnumerable keys, + CancellationToken token = default) + { + keys = Check.NotNull(keys, nameof(keys)); + + await GetAndRefreshManyAsync(keys, false, token); + } + + public void RemoveMany(IEnumerable keys) + { + keys = Check.NotNull(keys, nameof(keys)); + + Connect(); + + RedisDatabase.KeyDelete(keys.Select(key => (RedisKey)(Instance + key)).ToArray()); + } + + public async Task RemoveManyAsync(IEnumerable keys, CancellationToken token = default) + { + keys = Check.NotNull(keys, nameof(keys)); + + token.ThrowIfCancellationRequested(); + await ConnectAsync(token); + + await RedisDatabase.KeyDeleteAsync(keys.Select(key => (RedisKey)(Instance + key)).ToArray()); + } + protected virtual byte[][] GetAndRefreshMany( IEnumerable keys, bool getData) 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 d62411628e..1337206464 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -527,6 +527,198 @@ namespace Volo.Abp.Caching return value; } + public KeyValuePair[] GetOrAddMany( + IEnumerable keys, + Func, List>> factory, + Func optionsFactory = null, + bool? hideErrors = null, + bool considerUow = false) + { + + KeyValuePair[] result; + var keyArray = keys.ToArray(); + + var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems; + if (cacheSupportsMultipleItems == null) + { + result = GetManyFallback( + keyArray, + hideErrors, + considerUow + ); + } + else + { + var notCachedKeys = new List(); + var cachedValues = new List>(); + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + foreach (var key in keyArray) + { + var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull(); + if (value != null) + { + cachedValues.Add(new KeyValuePair(key, value)); + } + } + + notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList(); + if (!notCachedKeys.Any()) + { + return cachedValues.ToArray(); + } + } + + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + byte[][] cachedBytes; + + var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray; + try + { + cachedBytes = cacheSupportsMultipleItems.GetMany(readKeys.Select(NormalizeKey)); + } + catch (Exception ex) + { + if (hideErrors == true) + { + HandleException(ex); + return ToCacheItemsWithDefaultValues(keyArray); + } + + throw; + } + + result = cachedValues.Concat(ToCacheItems(cachedBytes, readKeys)).ToArray(); + } + + if (result.All(x => x.Value != null)) + { + return result; + } + + var missingKeys = new List(); + var missingValuesIndex = new List(); + for (var i = 0; i < keyArray.Length; i++) + { + if (result[i].Value != null) + { + continue; + } + + missingKeys.Add(keyArray[i]); + missingValuesIndex.Add(i); + } + + var missingValues = factory.Invoke(missingKeys).ToArray(); + var valueQueue = new Queue>(missingValues); + + SetMany(missingValues, optionsFactory?.Invoke(), hideErrors, considerUow); + + foreach (var index in missingValuesIndex) + { + result[index] = valueQueue.Dequeue(); + } + + return result; + } + + + public async Task[]> GetOrAddManyAsync( + IEnumerable keys, + Func, Task>>> factory, + Func optionsFactory = null, + bool? hideErrors = null, + bool considerUow = false, + CancellationToken token = default) + { + KeyValuePair[] result; + var keyArray = keys.ToArray(); + + var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems; + if (cacheSupportsMultipleItems == null) + { + result = await GetManyFallbackAsync( + keyArray, + hideErrors, + considerUow, token); + } + else + { + var notCachedKeys = new List(); + var cachedValues = new List>(); + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + foreach (var key in keyArray) + { + var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull(); + if (value != null) + { + cachedValues.Add(new KeyValuePair(key, value)); + } + } + + notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList(); + if (!notCachedKeys.Any()) + { + return cachedValues.ToArray(); + } + } + + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + byte[][] cachedBytes; + + var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray; + try + { + cachedBytes = await cacheSupportsMultipleItems.GetManyAsync(readKeys.Select(NormalizeKey), token); + } + catch (Exception ex) + { + if (hideErrors == true) + { + await HandleExceptionAsync(ex); + return ToCacheItemsWithDefaultValues(keyArray); + } + + throw; + } + + result = cachedValues.Concat(ToCacheItems(cachedBytes, readKeys)).ToArray(); + } + + if (result.All(x => x.Value != null)) + { + return result; + } + + var missingKeys = new List(); + var missingValuesIndex = new List(); + for (var i = 0; i < keyArray.Length; i++) + { + if (result[i].Value != null) + { + continue; + } + + missingKeys.Add(keyArray[i]); + missingValuesIndex.Add(i); + } + + var missingValues = (await factory.Invoke(missingKeys)).ToArray(); + var valueQueue = new Queue>(missingValues); + + await SetManyAsync(missingValues, optionsFactory?.Invoke(), hideErrors, considerUow, token); + + foreach (var index in missingValuesIndex) + { + result[index] = valueQueue.Dequeue(); + } + + return result; + } + /// /// Sets the cache item value for the provided key. /// @@ -924,6 +1116,71 @@ namespace Volo.Abp.Caching } } + public virtual void RefreshMany( + IEnumerable keys, + bool? hideErrors = null) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + if (Cache is ICacheSupportsMultipleItems cacheSupportsMultipleItems) + { + cacheSupportsMultipleItems.RefreshMany(keys.Select(NormalizeKey)); + } + else + { + foreach (var key in keys) + { + Cache.Refresh(NormalizeKey(key)); + } + } + } + catch (Exception ex) + { + if (hideErrors == true) + { + HandleException(ex); + return; + } + + throw; + } + } + + public virtual async Task RefreshManyAsync( + IEnumerable keys, + bool? hideErrors = null, + CancellationToken token = default) + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + if (Cache is ICacheSupportsMultipleItems cacheSupportsMultipleItems) + { + await cacheSupportsMultipleItems.RefreshManyAsync(keys.Select(NormalizeKey), token); + } + else + { + foreach (var key in keys) + { + await Cache.RefreshAsync(NormalizeKey(key), token); + } + } + } + catch (Exception ex) + { + if (hideErrors == true) + { + await HandleExceptionAsync(ex); + return; + } + + throw; + } + } + /// /// Removes the cache item for given key from cache. /// @@ -1027,6 +1284,130 @@ namespace Volo.Abp.Caching } } + public void RemoveMany( + IEnumerable keys, + bool? hideErrors = null, + bool considerUow = false) + { + var keyArray = keys.ToArray(); + + if (Cache is ICacheSupportsMultipleItems cacheSupportsMultipleItems) + { + void RemoveRealCache() + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + cacheSupportsMultipleItems.RemoveMany( + keyArray.Select(NormalizeKey) + ); + } + catch (Exception ex) + { + if (hideErrors == true) + { + HandleException(ex); + return; + } + + throw; + } + } + + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + + foreach (var key in keyArray) + { + if (uowCache.TryGetValue(key, out _)) + { + uowCache[key].RemoveValue(); + } + } + + // ReSharper disable once PossibleNullReferenceException + UnitOfWorkManager.Current.OnCompleted(() => + { + RemoveRealCache(); + return Task.CompletedTask; + }); + } + else + { + RemoveRealCache(); + } + } + else + { + foreach (var key in keyArray) + { + Remove(key, hideErrors, considerUow); + } + } + } + + public async Task RemoveManyAsync( + IEnumerable keys, + bool? hideErrors = null, + bool considerUow = false, + CancellationToken token = default) + { + var keyArray = keys.ToArray(); + + if (Cache is ICacheSupportsMultipleItems cacheSupportsMultipleItems) + { + async Task RemoveRealCache() + { + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + await cacheSupportsMultipleItems.RemoveManyAsync( + 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(); + } + } + + // ReSharper disable once PossibleNullReferenceException + UnitOfWorkManager.Current.OnCompleted(RemoveRealCache); + } + else + { + await RemoveRealCache(); + } + } + else + { + foreach (var key in keyArray) + { + await RemoveAsync(key, hideErrors, considerUow, token); + } + } + } + protected virtual void HandleException(Exception ex) { _ = HandleExceptionAsync(ex); diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/ICacheSupportsMultipleItems.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/ICacheSupportsMultipleItems.cs index af6f87021b..857385699d 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/ICacheSupportsMultipleItems.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/ICacheSupportsMultipleItems.cs @@ -25,6 +25,24 @@ namespace Volo.Abp.Caching IEnumerable> items, DistributedCacheEntryOptions options, CancellationToken token = default - ); + ); + + void RefreshMany( + IEnumerable keys + ); + + Task RefreshManyAsync( + IEnumerable keys, + CancellationToken token = default + ); + + void RemoveMany( + IEnumerable keys + ); + + Task RemoveManyAsync( + IEnumerable keys, + CancellationToken token = default + ); } -} \ 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 2f74fd678b..f8e76fd9dd 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs @@ -14,8 +14,8 @@ namespace Volo.Abp.Caching public interface IDistributedCache : IDistributedCache where TCacheItem : class { - } + /// /// Represents a distributed cache of type. /// Uses a generic cache key type of type. @@ -128,6 +128,44 @@ namespace Volo.Abp.Caching CancellationToken token = default ); + /// + /// Gets or Adds multiple cache items with the given keys. If any cache items not found for the given keys then adds cache items + /// provided by delegate and returns the provided cache items. + /// + /// The keys of cached items to be retrieved from the cache. + /// The factory delegate is used to provide the cache items when no cache items are 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 cache items. + KeyValuePair[] GetOrAddMany( + IEnumerable keys, + Func, List>> factory, + Func optionsFactory = null, + bool? hideErrors = null, + bool considerUow = false + ); + + /// + /// Gets or Adds multiple cache items with the given keys. If any cache items not found for the given keys then adds cache items + /// provided by delegate and returns the provided cache items. + /// + /// The keys of cached items to be retrieved from the cache. + /// The factory delegate is used to provide the cache items when no cache items are 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 items. + Task[]> GetOrAddManyAsync( + IEnumerable keys, + Func, Task>>> factory, + Func optionsFactory = null, + bool? hideErrors = null, + bool considerUow = false, + CancellationToken token = default + ); + /// /// Sets the cache item value for the provided key. /// @@ -219,6 +257,29 @@ namespace Volo.Abp.Caching CancellationToken token = default ); + /// + /// Refreshes the cache value of the given keys, and resets their sliding expiration timeout. + /// Based on the implementation, this can be more efficient than setting multiple items individually. + /// + /// The keys of cached items to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + void RefreshMany( + IEnumerable keys, + bool? hideErrors = null); + + /// + /// Refreshes the cache value of the given keys, and resets their sliding expiration timeout. + /// Based on the implementation, this can be more efficient than setting multiple items individually. + /// + /// The keys of cached items 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 RefreshManyAsync( + IEnumerable keys, + bool? hideErrors = null, + CancellationToken token = default); + /// /// Removes the cache item for given key from cache. /// @@ -245,5 +306,32 @@ namespace Volo.Abp.Caching 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. + void RemoveMany( + IEnumerable keys, + bool? hideErrors = null, + bool considerUow = false + ); + + /// + /// 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/DistributedCache_Tests.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs index 32876c08fe..c1d5af64ef 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 @@ -712,5 +712,240 @@ namespace Volo.Abp.Caching (await personCache.GetAsync("john")).Name.ShouldBe("John Nash"); (await personCache.GetAsync("baris")).ShouldBeNull(); } + + [Fact] + public async Task Should_Get_And_Add_Multiple_Items_Async() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + + var testkey3 = new[] {testkey, testkey2}; + var personCache = GetRequiredService>(); + + await personCache.SetAsync(testkey, new PersonCacheItem("john")); + + var cacheValue = await personCache.GetManyAsync(testkey3); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.ShouldBeNull(); + + cacheValue = await personCache.GetOrAddManyAsync(testkey3, (missingKeys) => + { + var missingKeyArray = missingKeys.ToArray(); + missingKeyArray.Length.ShouldBe(1); + missingKeyArray[0].ShouldBe(testkey2); + + return Task.FromResult(new List> + { + new(testkey2, new PersonCacheItem("jack")) + }); + }); + + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + + cacheValue = await personCache.GetManyAsync(testkey3); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + } + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_GetOrAddManyAsync() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + + var testkey3 = new[] {testkey, testkey2}; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetOrAddManyAsync(testkey3, (missingKeys) => Task.FromResult(new List> + { + new(testkey, new PersonCacheItem("john")), + new(testkey2, new PersonCacheItem("jack")), + }), considerUow: true); + + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: true); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: false); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + }); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_GetOrAddManyAsync() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + + var testkey3 = new[] {testkey, testkey2}; + + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetManyAsync(testkey3, considerUow: false); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + + using (var uow = GetRequiredService().Begin()) + { + cacheValue = await personCache.GetOrAddManyAsync(testkey3, (missingKeys) => Task.FromResult(new List> + { + new(testkey, new PersonCacheItem("john")), + new(testkey2, new PersonCacheItem("jack")), + }), considerUow: true); + + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: true); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: false); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + } + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: false); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + } + + [Fact] + public async Task Should_Remove_Multiple_Items_Async() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + var testkey3 = new[] {testkey, testkey2}; + + var personCache = GetRequiredService>(); + + await personCache.SetManyAsync(new List> + { + new(testkey, new PersonCacheItem("john")), + new(testkey2, new PersonCacheItem("jack")) + }); + + await personCache.RemoveManyAsync(testkey3); + + var cacheValue = await personCache.GetManyAsync(testkey3); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + } + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_RemoveManyAsync() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + var testkey3 = new[] {testkey, testkey2}; + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetManyAsync(testkey3, considerUow: false); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + + using (var uow = GetRequiredService().Begin()) + { + await personCache.SetManyAsync(new List> + { + new(testkey, new PersonCacheItem("john")), + new(testkey2, new PersonCacheItem("jack")) + }); + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: true); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + + await personCache.RemoveManyAsync(testkey3, considerUow: true); + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: false); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + }); + + await uow.CompleteAsync(); + } + + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_RemoveManyAsync() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + var testkey3 = new[] {testkey, testkey2}; + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetManyAsync(testkey3, considerUow: false); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + + using (var uow = GetRequiredService().Begin()) + { + await personCache.SetManyAsync(new List> + { + new(testkey, new PersonCacheItem("john")), + new(testkey2, new PersonCacheItem("jack")) + }, considerUow: true); + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: true); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.Name.ShouldBe("john"); + cacheValue[1].Value.Name.ShouldBe("jack"); + + await personCache.RemoveManyAsync(testkey3, considerUow: true); + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: true); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + } + + cacheValue = await personCache.GetManyAsync(testkey3, considerUow: true); + cacheValue.Length.ShouldBe(2); + cacheValue[0].Value.ShouldBeNull(); + cacheValue[1].Value.ShouldBeNull(); + } } } diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/TestMemoryDistributedCache.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/TestMemoryDistributedCache.cs index 69589d97cd..5df5b488d9 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/TestMemoryDistributedCache.cs +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/TestMemoryDistributedCache.cs @@ -57,5 +57,37 @@ namespace Volo.Abp.Caching await SetAsync(item.Key, item.Value, options, token); } } + + public void RefreshMany(IEnumerable keys) + { + foreach (var key in keys) + { + Refresh(key); + } + } + + public async Task RefreshManyAsync(IEnumerable keys, CancellationToken token = default) + { + foreach (var key in keys) + { + await RefreshAsync(key, token); + } + } + + public void RemoveMany(IEnumerable keys) + { + foreach (var key in keys) + { + Remove(key); + } + } + + public async Task RemoveManyAsync(IEnumerable keys, CancellationToken token = default) + { + foreach (var key in keys) + { + await RemoveAsync(key, token); + } + } } }