using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Primitives; using Nop.Core.Configuration; namespace Nop.Core.Caching; /// /// Represents a memory cache manager /// /// /// This class should be registered on IoC as singleton instance /// public partial class MemoryCacheManager : CacheKeyService, IStaticCacheManager { #region Fields // Flag: Has Dispose already been called? protected bool _disposed; protected readonly IMemoryCache _memoryCache; /// /// Holds the keys known by this nopCommerce instance /// protected readonly ICacheKeyManager _keyManager; protected static CancellationTokenSource _clearToken = new(); #endregion #region Ctor public MemoryCacheManager(AppSettings appSettings, IMemoryCache memoryCache, ICacheKeyManager cacheKeyManager) : base(appSettings) { _memoryCache = memoryCache; _keyManager = cacheKeyManager; } #endregion #region Utilities /// /// Prepare cache entry options for the passed key /// /// Cache key /// Cache entry options protected virtual MemoryCacheEntryOptions PrepareEntryOptions(CacheKey key) { //set expiration time for the passed cache key var options = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(key.CacheTime) }; //add token to clear cache entries options.AddExpirationToken(new CancellationChangeToken(_clearToken.Token)); options.RegisterPostEvictionCallback(OnEviction); _keyManager.AddKey(key.Key); return options; } /// /// The callback method which gets called when a cache entry expires. /// /// The key of the entry being evicted. /// The value of the entry being evicted. /// The . /// The information that was passed when registering the callback. protected virtual void OnEviction(object key, object value, EvictionReason reason, object state) { switch (reason) { // we clean up after ourselves elsewhere case EvictionReason.Removed: case EvictionReason.Replaced: case EvictionReason.TokenExpired: break; // if the entry was evicted by the cache itself, we remove the key default: _keyManager.RemoveKey(key as string); break; } } #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 Task RemoveAsync(CacheKey cacheKey, params object[] cacheKeyParameters) { var key = PrepareKey(cacheKey, cacheKeyParameters).Key; _memoryCache.Remove(key); _keyManager.RemoveKey(key); return Task.CompletedTask; } /// /// 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 ((key?.CacheTime ?? 0) <= 0) return await acquire(); var task = _memoryCache.GetOrCreate( key.Key, entry => { entry.SetOptions(PrepareEntryOptions(key)); return new Lazy>(acquire, true); }); try { return await task!.Value; } catch { //if a cached function throws an exception, remove it from the cache await RemoveAsync(key); throw; } } /// /// Get a cached item. If it's not in the cache yet, return a default value /// /// Type of cached item /// Cache key /// A default value to return if the key is not present in the cache /// /// A task that represents the asynchronous operation /// The task result contains the cached value associated with the specified key, or the default value if none was found /// public async Task GetAsync(CacheKey key, T defaultValue = default) { var value = _memoryCache.Get>>(key.Key)?.Value; try { return value != null ? await value : defaultValue; } catch { //if a cached function throws an exception, remove it from the cache await RemoveAsync(key); throw; } } /// /// 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) { return await GetAsync(key, () => Task.FromResult(acquire())); } /// /// 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) { var entry = _memoryCache.Get(key.Key); if (entry == null) return null; try { if (entry.GetType().GetProperty("Value")?.GetValue(entry) is not Task task) return null; await task; return task.GetType().GetProperty("Result")!.GetValue(task); } catch { //if a cached function throws an exception, remove it from the cache await RemoveAsync(key); throw; } } /// /// Add the specified key and object to the cache /// /// Key of cached item /// Value for caching /// A task that represents the asynchronous operation public Task SetAsync(CacheKey key, T data) { if (data != null && (key?.CacheTime ?? 0) > 0) _memoryCache.Set( key.Key, new Lazy>(() => Task.FromResult(data), true), PrepareEntryOptions(key)); return Task.CompletedTask; } /// /// Remove items by cache key prefix /// /// Cache key prefix /// Parameters to create cache key prefix /// A task that represents the asynchronous operation public Task RemoveByPrefixAsync(string prefix, params object[] prefixParameters) { foreach (var key in _keyManager.RemoveByPrefix(PrepareKeyPrefix(prefix, prefixParameters))) _memoryCache.Remove(key); return Task.CompletedTask; } /// /// Clear all cache data /// /// A task that represents the asynchronous operation public Task ClearAsync() { _clearToken.Cancel(); _clearToken.Dispose(); _clearToken = new CancellationTokenSource(); _keyManager.Clear(); return Task.CompletedTask; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } // Protected implementation of Dispose pattern. protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) // don't dispose of the MemoryCache, as it is injected _clearToken.Dispose(); _disposed = true; } #endregion }