using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.Caching; using System.Text.RegularExpressions; using System.Threading; using System.Web.Caching; using Umbraco.Core.Logging; using CacheItemPriority = System.Web.Caching.CacheItemPriority; namespace Umbraco.Core.Cache { /// /// A cache provider that wraps the logic of a System.Runtime.Caching.ObjectCache /// internal class ObjectCacheRuntimeCacheProvider : IRuntimeCacheProvider { private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); internal ObjectCache MemoryCache; // an object that represent a value that has not been created yet protected readonly object ValueNotCreated = new object(); /// /// Used for debugging /// internal Guid InstanceId { get; private set; } public ObjectCacheRuntimeCacheProvider() { MemoryCache = new MemoryCache("in-memory"); InstanceId = Guid.NewGuid(); } protected object GetSafeLazyValue(Lazy lazy, bool onlyIfValueIsCreated = false) { try { // if onlyIfValueIsCreated, do not trigger value creation // must return something, though, to differenciate from null values if (onlyIfValueIsCreated && lazy.IsValueCreated == false) return ValueNotCreated; return lazy.Value; } catch { return null; } } #region Clear public virtual void ClearAllCache() { using (new WriteLock(_locker)) { MemoryCache.DisposeIfDisposable(); MemoryCache = new MemoryCache("in-memory"); } } public virtual void ClearCacheItem(string key) { using (new WriteLock(_locker)) { if (MemoryCache[key] == null) return; MemoryCache.Remove(key); } } public virtual void ClearCacheObjectTypes(string typeName) { using (new WriteLock(_locker)) { foreach (var key in MemoryCache .Where(x => { // x.Value is Lazy and not null, its value may be null // remove null values as well, does not hurt // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); return value == null || value.GetType().ToString().InvariantEquals(typeName); }) .Select(x => x.Key) .ToArray()) // ToArray required to remove MemoryCache.Remove(key); } } public virtual void ClearCacheObjectTypes() { using (new WriteLock(_locker)) { var typeOfT = typeof (T); foreach (var key in MemoryCache .Where(x => { // x.Value is Lazy and not null, its value may be null // remove null values as well, does not hurt // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 // until then we will check if 'T' is an interface and if so we will use the 'is' clause, // otherwise we do an exact match. return value == null || (typeOfT.IsInterface ? (value is T) : value.GetType() == typeOfT); }) .Select(x => x.Key) .ToArray()) // ToArray required to remove MemoryCache.Remove(key); } } public virtual void ClearCacheObjectTypes(Func predicate) { using (new WriteLock(_locker)) { var typeOfT = typeof(T); foreach (var key in MemoryCache .Where(x => { // x.Value is Lazy and not null, its value may be null // remove null values as well, does not hurt // get non-created as NonCreatedValue & exceptions as null var value = GetSafeLazyValue((Lazy)x.Value, true); if (value == null) return true; //TODO: waiting on a response for this comment: https://github.com/umbraco/Umbraco-CMS/commit/c2db7b2b9b78847a828512818e79492ecc24ac7c#commitcomment-9492329 // until then we will check if 'T' is an interface and if so we will use the 'is' clause, // otherwise we do an exact match. return ((typeOfT.IsInterface && value is T) || (value.GetType() == typeOfT)) && predicate(x.Key, (T)value); }) .Select(x => x.Key) .ToArray()) // ToArray required to remove MemoryCache.Remove(key); } } public virtual void ClearCacheByKeySearch(string keyStartsWith) { using (new WriteLock(_locker)) { foreach (var key in MemoryCache .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) .Select(x => x.Key) .ToArray()) // ToArray required to remove MemoryCache.Remove(key); } } public virtual void ClearCacheByKeyExpression(string regexString) { using (new WriteLock(_locker)) { foreach (var key in MemoryCache .Where(x => Regex.IsMatch(x.Key, regexString)) .Select(x => x.Key) .ToArray()) // ToArray required to remove MemoryCache.Remove(key); } } #endregion #region Get public IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) { KeyValuePair[] entries; using (new ReadLock(_locker)) { entries = MemoryCache .Where(x => x.Key.InvariantStartsWith(keyStartsWith)) .ToArray(); // evaluate while locked } return entries .Select(x => GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null .Where(x => x != null) // backward compat, don't store null values in the cache .ToList(); } public IEnumerable GetCacheItemsByKeyExpression(string regexString) { KeyValuePair[] entries; using (new ReadLock(_locker)) { entries = MemoryCache .Where(x => Regex.IsMatch(x.Key, regexString)) .ToArray(); // evaluate while locked } return entries .Select(x => GetSafeLazyValue((Lazy)x.Value)) // return exceptions as null .Where(x => x != null) // backward compat, don't store null values in the cache .ToList(); } public object GetCacheItem(string cacheKey) { Lazy result; using (new ReadLock(_locker)) { result = MemoryCache.Get(cacheKey) as Lazy; // null if key not found } return result == null ? null : GetSafeLazyValue(result); // return exceptions as null } public object GetCacheItem(string cacheKey, Func getCacheItem) { return GetCacheItem(cacheKey, getCacheItem, null); } public object GetCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal,CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { // see notes in HttpRuntimeCacheProvider Lazy result; using (var lck = new UpgradeableReadLock(_locker)) { result = MemoryCache.Get(cacheKey) as Lazy; if (result == null || GetSafeLazyValue(result, true) == null) // get non-created as NonCreatedValue & exceptions as null { result = new Lazy(getCacheItem); var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); lck.UpgradeToWriteLock(); MemoryCache.Set(cacheKey, result, policy); } } return result.Value; } #endregion #region Insert public void InsertCacheItem(string cacheKey, Func getCacheItem, TimeSpan? timeout = null, bool isSliding = false, CacheItemPriority priority = CacheItemPriority.Normal, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { // NOTE - here also we must insert a Lazy but we can evaluate it right now // and make sure we don't store a null value. var result = new Lazy(getCacheItem); var value = result.Value; // force evaluation now if (value == null) return; // do not store null values (backward compat) var policy = GetPolicy(timeout, isSliding, removedCallback, dependentFiles); MemoryCache.Set(cacheKey, result, policy); } #endregion private static CacheItemPolicy GetPolicy(TimeSpan? timeout = null, bool isSliding = false, CacheItemRemovedCallback removedCallback = null, string[] dependentFiles = null) { var absolute = isSliding ? ObjectCache.InfiniteAbsoluteExpiration : (timeout == null ? ObjectCache.InfiniteAbsoluteExpiration : DateTime.Now.Add(timeout.Value)); var sliding = isSliding == false ? ObjectCache.NoSlidingExpiration : (timeout ?? ObjectCache.NoSlidingExpiration); var policy = new CacheItemPolicy { AbsoluteExpiration = absolute, SlidingExpiration = sliding }; if (dependentFiles != null && dependentFiles.Any()) { policy.ChangeMonitors.Add(new HostFileChangeMonitor(dependentFiles.ToList())); } if (removedCallback != null) { policy.RemovedCallback = arguments => { //convert the reason var reason = CacheItemRemovedReason.Removed; switch (arguments.RemovedReason) { case CacheEntryRemovedReason.Removed: reason = CacheItemRemovedReason.Removed; break; case CacheEntryRemovedReason.Expired: reason = CacheItemRemovedReason.Expired; break; case CacheEntryRemovedReason.Evicted: reason = CacheItemRemovedReason.Underused; break; case CacheEntryRemovedReason.ChangeMonitorChanged: reason = CacheItemRemovedReason.Expired; break; case CacheEntryRemovedReason.CacheSpecificEviction: reason = CacheItemRemovedReason.Underused; break; } //call the callback removedCallback(arguments.CacheItem.Key, arguments.CacheItem.Value, reason); }; } return policy; } } }