using System.Collections.Concurrent; using Microsoft.Extensions.Caching.Distributed; using Newtonsoft.Json; using Nop.Core.Configuration; using Nop.Core.Infrastructure; namespace Nop.Core.Caching; /// /// Represents a base distributed cache /// public abstract class DistributedCacheManager : CacheKeyService, IStaticCacheManager { #region Fields /// /// Holds the keys known by this nopCommerce instance /// protected readonly ICacheKeyManager _localKeyManager; protected readonly IDistributedCache _distributedCache; protected readonly IConcurrentCollection _concurrentCollection; /// /// Holds ongoing acquisition tasks, used to avoid duplicating work /// protected readonly ConcurrentDictionary>> _ongoing = new(); #endregion #region Ctor protected DistributedCacheManager(AppSettings appSettings, IDistributedCache distributedCache, ICacheKeyManager cacheKeyManager, IConcurrentCollection concurrentCollection) : base(appSettings) { _distributedCache = distributedCache; _localKeyManager = cacheKeyManager; _concurrentCollection = concurrentCollection; } #endregion #region Utilities /// /// Clear all data on this instance /// /// A task that represents the asynchronous operation protected virtual void ClearInstanceData() { _concurrentCollection.Clear(); _localKeyManager.Clear(); } /// /// Remove items by cache key prefix /// /// Cache key prefix /// Parameters to create cache key prefix /// The removed keys protected virtual IEnumerable RemoveByPrefixInstanceData(string prefix, params object[] prefixParameters) { var keyPrefix = PrepareKeyPrefix(prefix, prefixParameters); _concurrentCollection.Prune(keyPrefix, out _); return _localKeyManager.RemoveByPrefix(keyPrefix); } /// /// Prepare cache entry options for the passed key /// /// Cache key /// Cache entry options protected virtual DistributedCacheEntryOptions PrepareEntryOptions(CacheKey key) { //set expiration time for the passed cache key return new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(key.CacheTime) }; } /// /// Add the specified key and object to the local cache /// /// Key of cached item /// Value for caching protected virtual void SetLocal(string key, object value) { _concurrentCollection.Add(key, value); _localKeyManager.AddKey(key); } /// /// Remove the value with the specified key from the cache /// /// Cache key protected virtual void RemoveLocal(string key) { _concurrentCollection.Remove(key); _localKeyManager.RemoveKey(key); } /// /// Try get a cached item. If it's not in the cache yet, then return default object /// /// Type of cached item /// Cache key protected virtual async Task<(bool isSet, T item)> TryGetItemAsync(string key) { var json = await _distributedCache.GetStringAsync(key); return string.IsNullOrEmpty(json) ? (false, default) : (true, item: JsonConvert.DeserializeObject(json)); } /// /// Remove the value with the specified key from the cache /// /// Cache key /// Remove from instance protected virtual async Task RemoveAsync(string key, bool removeFromInstance = true) { _ongoing.TryRemove(key, out _); await _distributedCache.RemoveAsync(key); if (!removeFromInstance) return; RemoveLocal(key); } #endregion #region Methods /// /// Remove the value with the specified key from the cache /// /// Cache key /// Parameters to create cache key /// A task that represents the asynchronous operation public async Task RemoveAsync(CacheKey cacheKey, params object[] cacheKeyParameters) { await RemoveAsync(PrepareKey(cacheKey, cacheKeyParameters).Key); } /// /// Get a cached item. If it's not in the cache yet, then load and cache it /// /// Type of cached item /// Cache key /// Function to load item if it's not in the cache yet /// /// A task that represents the asynchronous operation /// The task result contains the cached value associated with the specified key /// public async Task GetAsync(CacheKey key, Func> acquire) { if (_concurrentCollection.TryGetValue(key.Key, out var data)) return (T)data; var lazy = _ongoing.GetOrAdd(key.Key, _ => new(async () => await acquire(), true)); var setTask = Task.CompletedTask; try { if (lazy.IsValueCreated) return (T)await lazy.Value; var (isSet, item) = await TryGetItemAsync(key.Key); if (!isSet) { item = (T)await lazy.Value; if (key.CacheTime == 0 || item == null) return item; setTask = _distributedCache.SetStringAsync( key.Key, JsonConvert.SerializeObject(item), PrepareEntryOptions(key)); } SetLocal(key.Key, item); return item; } finally { _ = setTask.ContinueWith(_ => _ongoing.TryRemove(new KeyValuePair>>(key.Key, lazy))); } } /// /// Get a cached item. If it's not in the cache yet, then load and cache it /// /// Type of cached item /// Cache key /// Function to load item if it's not in the cache yet /// /// A task that represents the asynchronous operation /// The task result contains the cached value associated with the specified key /// public Task GetAsync(CacheKey key, Func acquire) { return GetAsync(key, () => Task.FromResult(acquire())); } public async Task GetAsync(CacheKey key, T defaultValue = default) { var value = await _distributedCache.GetStringAsync(key.Key); return value != null ? JsonConvert.DeserializeObject(value) : defaultValue; } /// /// Get a cached item as an instance, or null on a cache miss. /// /// Cache key /// /// A task that represents the asynchronous operation /// The task result contains the cached value associated with the specified key, or null if none was found /// public async Task GetAsync(CacheKey key) { return await GetAsync(key); } /// /// Add the specified key and object to the cache /// /// Key of cached item /// Value for caching /// A task that represents the asynchronous operation public async Task SetAsync(CacheKey key, T data) { if (data == null || (key?.CacheTime ?? 0) <= 0) return; var lazy = new Lazy>(() => Task.FromResult(data as object), true); try { _ongoing.TryAdd(key.Key, lazy); // await the lazy task in order to force value creation instead of directly setting data // this way, other cache manager instances can access it while it is being set SetLocal(key.Key, await lazy.Value); await _distributedCache.SetStringAsync(key.Key, JsonConvert.SerializeObject(data), PrepareEntryOptions(key)); } finally { _ongoing.TryRemove(new KeyValuePair>>(key.Key, lazy)); } } /// /// Remove items by cache key prefix /// /// Cache key prefix /// Parameters to create cache key prefix /// A task that represents the asynchronous operation public abstract Task RemoveByPrefixAsync(string prefix, params object[] prefixParameters); /// /// Clear all cache data /// /// A task that represents the asynchronous operation public abstract Task ClearAsync(); /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { GC.SuppressFinalize(this); } #endregion }