// // Copyright (c) 2009 Microsoft Corporation. All rights reserved. // using System; using System.Runtime.Caching.Configuration; using System.Runtime.Caching.Resources; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Collections.ObjectModel; using System.Configuration; using System.Diagnostics.CodeAnalysis; using System.Security; using System.Security.Permissions; using System.Threading; namespace System.Runtime.Caching { [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification= "The class represents a type of cache")] public class MemoryCache : ObjectCache, IEnumerable, IDisposable { private const DefaultCacheCapabilities CAPABILITIES = DefaultCacheCapabilities.InMemoryProvider | DefaultCacheCapabilities.CacheEntryChangeMonitors | DefaultCacheCapabilities.AbsoluteExpirations | DefaultCacheCapabilities.SlidingExpirations | DefaultCacheCapabilities.CacheEntryUpdateCallback | DefaultCacheCapabilities.CacheEntryRemovedCallback; private static readonly TimeSpan OneYear = new TimeSpan(365, 0, 0, 0); private static object s_initLock = new object(); private static MemoryCache s_defaultCache; private static CacheEntryRemovedCallback s_sentinelRemovedCallback = new CacheEntryRemovedCallback(SentinelEntry.OnCacheEntryRemovedCallback); private MemoryCacheStore[] _stores; private int _storeCount; private int _storeMask; private int _disposed; private MemoryCacheStatistics _stats; private string _name; private PerfCounters _perfCounters; private bool _configLess; EventHandler _onAppDomainUnload; UnhandledExceptionEventHandler _onUnhandledException; private bool IsDisposed { get { return (_disposed == 1); } } internal bool ConfigLess { get { return _configLess; } } private class SentinelEntry { private string _key; private ChangeMonitor _expensiveObjectDependency; private CacheEntryUpdateCallback _updateCallback; internal SentinelEntry(string key, ChangeMonitor expensiveObjectDependency, CacheEntryUpdateCallback callback) { _key = key; _expensiveObjectDependency = expensiveObjectDependency; _updateCallback = callback; } internal string Key { get { return _key; } } internal ChangeMonitor ExpensiveObjectDependency { get { return _expensiveObjectDependency; } } internal CacheEntryUpdateCallback CacheEntryUpdateCallback { get { return _updateCallback; } } private static bool IsPolicyValid(CacheItemPolicy policy) { if (policy == null) { return false; } // see if any change monitors have changed bool hasChanged = false; Collection changeMonitors = policy.ChangeMonitors; if (changeMonitors != null) { foreach (ChangeMonitor monitor in changeMonitors) { if (monitor != null && monitor.HasChanged) { hasChanged = true; break; } } } // if the monitors haven't changed yet and we have an update callback // then the policy is valid if (!hasChanged && policy.UpdateCallback != null) { return true; } // if the monitors have changed we need to dispose them if (hasChanged) { foreach (ChangeMonitor monitor in changeMonitors) { if (monitor != null) { monitor.Dispose(); } } } return false; } internal static void OnCacheEntryRemovedCallback(CacheEntryRemovedArguments arguments) { MemoryCache cache = arguments.Source as MemoryCache; SentinelEntry entry = arguments.CacheItem.Value as SentinelEntry; CacheEntryRemovedReason reason = arguments.RemovedReason; switch (reason) { case CacheEntryRemovedReason.Expired: break; case CacheEntryRemovedReason.ChangeMonitorChanged: if (entry.ExpensiveObjectDependency.HasChanged) { // If the expensiveObject has been removed explicitly by Cache.Remove, // return from the SentinelEntry removed callback // thus effectively removing the SentinelEntry from the cache. return; } break; case CacheEntryRemovedReason.Evicted: Dbg.Fail("Reason should never be CacheEntryRemovedReason.Evicted since the entry was inserted as NotRemovable."); return; default: // do nothing if reason is Removed or CacheSpecificEviction return; } // invoke update callback try { CacheEntryUpdateArguments args = new CacheEntryUpdateArguments(cache, reason, entry.Key, null); entry.CacheEntryUpdateCallback(args); Object expensiveObject = (args.UpdatedCacheItem != null) ? args.UpdatedCacheItem.Value : null; CacheItemPolicy policy = args.UpdatedCacheItemPolicy; // Dev10 861163 - Only update the "expensive" object if the user returns a new object, // a policy with update callback, and the change monitors haven't changed. (Inserting // with change monitors that have already changed will cause recursion.) if (expensiveObject != null && IsPolicyValid(policy)) { cache.Set(entry.Key, expensiveObject, policy); } else { cache.Remove(entry.Key); } } catch { cache.Remove(entry.Key); // Review: What should we do with this exception? } } } // private and internal internal MemoryCacheStore GetStore(MemoryCacheKey cacheKey) { // Dev10 865907: Math.Abs throws OverflowException for Int32.MinValue int hashCode = cacheKey.Hash; if (hashCode < 0) { hashCode = (hashCode == Int32.MinValue) ? 0 : -hashCode; } int idx = hashCode & _storeMask; return _stores[idx]; } [SecuritySafeCritical] [PermissionSet(SecurityAction.Assert, Unrestricted = true)] [SuppressMessage("Microsoft.Security", "CA2106:SecureAsserts", Justification = "Grandfathered suppression from original caching code checkin")] private void InitDisposableMembers(NameValueCollection config) { bool dispose = true; try { try { _perfCounters = new PerfCounters(_name); } catch { // ignore exceptions from perf counters } for (int i = 0; i < _stores.Length; i++) { _stores[i] = new MemoryCacheStore(this, _perfCounters); } _stats = new MemoryCacheStatistics(this, config); AppDomain appDomain = Thread.GetDomain(); EventHandler onAppDomainUnload = new EventHandler(OnAppDomainUnload); appDomain.DomainUnload += onAppDomainUnload; _onAppDomainUnload = onAppDomainUnload; UnhandledExceptionEventHandler onUnhandledException = new UnhandledExceptionEventHandler(OnUnhandledException); appDomain.UnhandledException += onUnhandledException; _onUnhandledException = onUnhandledException; dispose = false; } finally { if (dispose) { Dispose(); } } } private void OnAppDomainUnload(Object unusedObject, EventArgs unusedEventArgs) { Dispose(); } private void OnUnhandledException(Object sender, UnhandledExceptionEventArgs eventArgs) { // if the CLR is terminating, dispose the cache. // This will dispose the perf counters (see Dev10 680819). if (eventArgs.IsTerminating) { Dispose(); } } private void ValidatePolicy(CacheItemPolicy policy) { if (policy.AbsoluteExpiration != ObjectCache.InfiniteAbsoluteExpiration && policy.SlidingExpiration != ObjectCache.NoSlidingExpiration) { throw new ArgumentException(R.Invalid_expiration_combination, "policy"); } if (policy.SlidingExpiration < ObjectCache.NoSlidingExpiration || OneYear < policy.SlidingExpiration) { throw new ArgumentOutOfRangeException("policy", RH.Format(R.Argument_out_of_range, "SlidingExpiration", ObjectCache.NoSlidingExpiration, OneYear)); } if (policy.RemovedCallback != null && policy.UpdateCallback != null) { throw new ArgumentException(R.Invalid_callback_combination, "policy"); } if (policy.Priority != CacheItemPriority.Default && policy.Priority != CacheItemPriority.NotRemovable) { throw new ArgumentOutOfRangeException("policy", RH.Format(R.Argument_out_of_range, "Priority", CacheItemPriority.Default, CacheItemPriority.NotRemovable)); } } // public // Amount of memory that can be used before // the cache begins to forcibly remove items. public long CacheMemoryLimit { get { return _stats.CacheMemoryLimit; } } public static MemoryCache Default { get { if (s_defaultCache == null) { lock (s_initLock) { if (s_defaultCache == null) { s_defaultCache = new MemoryCache(); } } } return s_defaultCache; } } public override DefaultCacheCapabilities DefaultCacheCapabilities { get { return CAPABILITIES; } } public override string Name { get { return _name; } } // Percentage of physical memory that can be used before // the cache begins to forcibly remove items. public long PhysicalMemoryLimit { get { return _stats.PhysicalMemoryLimit; } } // The maximum interval of time afterwhich the cache // will update its memory statistics. public TimeSpan PollingInterval { get { return _stats.PollingInterval; } } // Only used for Default MemoryCache private MemoryCache() { _name = "Default"; Init(null); } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public MemoryCache(string name, NameValueCollection config = null) { if (name == null) { throw new ArgumentNullException("name"); } if (name == String.Empty) { throw new ArgumentException(R.Empty_string_invalid, "name"); } if (String.Equals(name, "default", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException(R.Default_is_reserved, "name"); } _name = name; Init(config); } // Configless is used when redirecting ASP.NET cache into the MemoryCache. This avoids infinite recursion // due to the fact that the (ASP.NET) config system uses the cache, and the cache uses the // config system. This method could be made public, perhaps with CAS to prevent partial trust callers. [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Grandfathered suppression from original caching code checkin")] internal MemoryCache(string name, NameValueCollection config, bool configLess) { if (name == null) { throw new ArgumentNullException("name"); } if (name == String.Empty) { throw new ArgumentException(R.Empty_string_invalid, "name"); } if (String.Equals(name, "default", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException(R.Default_is_reserved, "name"); } _name = name; _configLess = configLess; Init(config); } private void Init(NameValueCollection config) { _storeCount = Environment.ProcessorCount; #if MONO if (config != null) { if (config ["__MonoEmulateOneCPU"] == "true") _storeCount = 1; if (config ["__MonoTimerPeriod"] != null) { try { int parsed = (int)UInt32.Parse (config ["__MonoTimerPeriod"]); CacheExpires.EXPIRATIONS_INTERVAL = new TimeSpan (0, 0, parsed); } catch { // } } } #endif _storeMask = _storeCount - 1; _stores = new MemoryCacheStore[_storeCount]; InitDisposableMembers(config); } private object AddOrGetExistingInternal(string key, object value, CacheItemPolicy policy) { if (key == null) { throw new ArgumentNullException("key"); } DateTimeOffset absExp = ObjectCache.InfiniteAbsoluteExpiration; TimeSpan slidingExp = ObjectCache.NoSlidingExpiration; CacheItemPriority priority = CacheItemPriority.Default; Collection changeMonitors = null; CacheEntryRemovedCallback removedCallback = null; if (policy != null) { ValidatePolicy(policy); if (policy.UpdateCallback != null) { throw new ArgumentException(R.Update_callback_must_be_null, "policy"); } absExp = policy.AbsoluteExpiration; slidingExp = policy.SlidingExpiration; priority = policy.Priority; changeMonitors = policy.ChangeMonitors; removedCallback = policy.RemovedCallback; } if (IsDisposed) { if (changeMonitors != null) { foreach (ChangeMonitor monitor in changeMonitors) { if (monitor != null) { monitor.Dispose(); } } } return null; } MemoryCacheKey cacheKey = new MemoryCacheKey(key); MemoryCacheStore store = GetStore(cacheKey); MemoryCacheEntry entry = store.AddOrGetExisting(cacheKey, new MemoryCacheEntry(key, value, absExp, slidingExp, priority, changeMonitors, removedCallback, this)); return (entry != null) ? entry.Value : null; } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override CacheEntryChangeMonitor CreateCacheEntryChangeMonitor(IEnumerable keys, String regionName = null) { if (regionName != null) { throw new NotSupportedException(R.RegionName_not_supported); } if (keys == null) { throw new ArgumentNullException("keys"); } List keysClone = new List(keys); if (keysClone.Count == 0) { throw new ArgumentException(RH.Format(R.Empty_collection, "keys")); } foreach (string key in keysClone) { if (key == null) { throw new ArgumentException(RH.Format(R.Collection_contains_null_element, "keys")); } } return new MemoryCacheEntryChangeMonitor(keysClone.AsReadOnly(), regionName, this); } public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) == 0) { // unhook domain events DisposeSafeCritical(); // stats must be disposed prior to disposing the stores. if (_stats != null) { _stats.Dispose(); } if (_stores != null) { foreach (MemoryCacheStore store in _stores) { if (store != null) { store.Dispose(); } } } if (_perfCounters != null) { _perfCounters.Dispose(); } GC.SuppressFinalize(this); } } [SecuritySafeCritical] [PermissionSet(SecurityAction.Assert, Unrestricted = true)] [SuppressMessage("Microsoft.Security", "CA2106:SecureAsserts", Justification = "Grandfathered suppression from original caching code checkin")] private void DisposeSafeCritical() { AppDomain appDomain = Thread.GetDomain(); if (_onAppDomainUnload != null) { appDomain.DomainUnload -= _onAppDomainUnload; } if (_onUnhandledException != null) { appDomain.UnhandledException -= _onUnhandledException; } } private object GetInternal(string key, string regionName) { if (regionName != null) { throw new NotSupportedException(R.RegionName_not_supported); } if (key == null) { throw new ArgumentNullException("key"); } MemoryCacheEntry entry = GetEntry(key); return (entry != null) ? entry.Value : null; } internal MemoryCacheEntry GetEntry(String key) { if (IsDisposed) { return null; } MemoryCacheKey cacheKey = new MemoryCacheKey(key); MemoryCacheStore store = GetStore(cacheKey); return store.Get(cacheKey); } IEnumerator IEnumerable.GetEnumerator() { Hashtable h = new Hashtable(); if (!IsDisposed) { foreach (MemoryCacheStore store in _stores) { store.CopyTo(h); } } return h.GetEnumerator(); } protected override IEnumerator> GetEnumerator() { Dictionary h = new Dictionary(); if (!IsDisposed) { foreach (MemoryCacheStore store in _stores) { store.CopyTo(h); } } return h.GetEnumerator(); } internal MemoryCacheEntry RemoveEntry(string key, MemoryCacheEntry entry, CacheEntryRemovedReason reason) { MemoryCacheKey cacheKey = new MemoryCacheKey(key); MemoryCacheStore store = GetStore(cacheKey); return store.Remove(cacheKey, entry, reason); } public long Trim(int percent) { if (percent > 100) { percent = 100; } long trimmed = 0; if (_disposed == 0) { foreach (MemoryCacheStore store in _stores) { trimmed += store.TrimInternal(percent); } } return trimmed; } //Default indexer property public override object this[string key] { get { return GetInternal(key, null); } set { Set(key, value, ObjectCache.InfiniteAbsoluteExpiration); } } //Existence check for a single item [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override bool Contains(string key, string regionName = null) { return (GetInternal(key, regionName) != null); } // Dev10 907758: Breaking bug in System.RuntimeCaching.MemoryCache.AddOrGetExisting (CacheItem, CacheItemPolicy) public override bool Add(CacheItem item, CacheItemPolicy policy) { CacheItem existingEntry = AddOrGetExisting(item, policy); return (existingEntry == null || existingEntry.Value == null); } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null) { if (regionName != null) { throw new NotSupportedException(R.RegionName_not_supported); } CacheItemPolicy policy = new CacheItemPolicy(); policy.AbsoluteExpiration = absoluteExpiration; return AddOrGetExistingInternal(key, value, policy); } public override CacheItem AddOrGetExisting(CacheItem item, CacheItemPolicy policy) { if (item == null) { throw new ArgumentNullException("item"); } return new CacheItem(item.Key, AddOrGetExistingInternal(item.Key, item.Value, policy)); } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override object AddOrGetExisting(string key, object value, CacheItemPolicy policy, string regionName = null) { if (regionName != null) { throw new NotSupportedException(R.RegionName_not_supported); } return AddOrGetExistingInternal(key, value, policy); } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override object Get(string key, string regionName = null) { return GetInternal(key, regionName); } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override CacheItem GetCacheItem(string key, string regionName = null) { object value = GetInternal(key, regionName); return (value != null) ? new CacheItem(key, value) : null; } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override void Set(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null) { if (regionName != null) { throw new NotSupportedException(R.RegionName_not_supported); } CacheItemPolicy policy = new CacheItemPolicy(); policy.AbsoluteExpiration = absoluteExpiration; Set(key, value, policy); } public override void Set(CacheItem item, CacheItemPolicy policy) { if (item == null) { throw new ArgumentNullException("item"); } Set(item.Key, item.Value, policy); } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override void Set(string key, object value, CacheItemPolicy policy, string regionName = null) { if (regionName != null) { throw new NotSupportedException(R.RegionName_not_supported); } if (key == null) { throw new ArgumentNullException("key"); } DateTimeOffset absExp = ObjectCache.InfiniteAbsoluteExpiration; TimeSpan slidingExp = ObjectCache.NoSlidingExpiration; CacheItemPriority priority = CacheItemPriority.Default; Collection changeMonitors = null; CacheEntryRemovedCallback removedCallback = null; if (policy != null) { ValidatePolicy(policy); if (policy.UpdateCallback != null) { Set(key, value, policy.ChangeMonitors, policy.AbsoluteExpiration, policy.SlidingExpiration, policy.UpdateCallback); return; } absExp = policy.AbsoluteExpiration; slidingExp = policy.SlidingExpiration; priority = policy.Priority; changeMonitors = policy.ChangeMonitors; removedCallback = policy.RemovedCallback; } if (IsDisposed) { if (changeMonitors != null) { foreach (ChangeMonitor monitor in changeMonitors) { if (monitor != null) { monitor.Dispose(); } } } return; } MemoryCacheKey cacheKey = new MemoryCacheKey(key); MemoryCacheStore store = GetStore(cacheKey); store.Set(cacheKey, new MemoryCacheEntry(key, value, absExp, slidingExp, priority, changeMonitors, removedCallback, this)); } // DevDiv Bugs 162763: // Add a an event that fires *before* an item is evicted from the ASP.NET Cache internal void Set(string key, object value, Collection changeMonitors, DateTimeOffset absoluteExpiration, TimeSpan slidingExpiration, CacheEntryUpdateCallback onUpdateCallback) { if (key == null) { throw new ArgumentNullException("key"); } if (changeMonitors == null && absoluteExpiration == ObjectCache.InfiniteAbsoluteExpiration && slidingExpiration == ObjectCache.NoSlidingExpiration) { throw new ArgumentException(R.Invalid_argument_combination); } if (onUpdateCallback == null) { throw new ArgumentNullException("onUpdateCallback"); } if (IsDisposed) { if (changeMonitors != null) { foreach (ChangeMonitor monitor in changeMonitors) { if (monitor != null) { monitor.Dispose(); } } } return; } // Insert updatable cache entry MemoryCacheKey cacheKey = new MemoryCacheKey(key); MemoryCacheStore store = GetStore(cacheKey); MemoryCacheEntry cacheEntry = new MemoryCacheEntry(key, value, ObjectCache.InfiniteAbsoluteExpiration, ObjectCache.NoSlidingExpiration, CacheItemPriority.NotRemovable, null, null, this); store.Set(cacheKey, cacheEntry); // Ensure the sentinel depends on its updatable entry string[] cacheKeys = { key }; ChangeMonitor expensiveObjectDep = CreateCacheEntryChangeMonitor(cacheKeys); if (changeMonitors == null) { changeMonitors = new Collection(); } changeMonitors.Add(expensiveObjectDep); // Insert sentinel entry for the updatable cache entry MemoryCacheKey sentinelCacheKey = new MemoryCacheKey("OnUpdateSentinel" + key); MemoryCacheStore sentinelStore = GetStore(sentinelCacheKey); MemoryCacheEntry sentinelCacheEntry = new MemoryCacheEntry(sentinelCacheKey.Key, new SentinelEntry(key, expensiveObjectDep, onUpdateCallback), absoluteExpiration, slidingExpiration, CacheItemPriority.NotRemovable, changeMonitors, s_sentinelRemovedCallback, this); sentinelStore.Set(sentinelCacheKey, sentinelCacheEntry); cacheEntry.ConfigureUpdateSentinel(sentinelStore, sentinelCacheEntry); } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override object Remove(string key, string regionName = null) { if (regionName != null) { throw new NotSupportedException(R.RegionName_not_supported); } if (key == null) { throw new ArgumentNullException("key"); } if (IsDisposed) { return null; } MemoryCacheEntry entry = RemoveEntry(key, null, CacheEntryRemovedReason.Removed); return (entry != null) ? entry.Value : null; } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override long GetCount(string regionName = null) { if (regionName != null) { throw new NotSupportedException(R.RegionName_not_supported); } long count = 0; if (!IsDisposed) { foreach (MemoryCacheStore store in _stores) { count += store.Count; } } return count; } [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification="This is assembly is a special case approved by the NetFx API review board")] public override IDictionary GetValues(IEnumerable keys, string regionName = null) { if (regionName != null) { throw new NotSupportedException(R.RegionName_not_supported); } if (keys == null) { throw new ArgumentNullException("keys"); } Dictionary values = null; if (!IsDisposed) { foreach (string key in keys) { if (key == null) { throw new ArgumentException(RH.Format(R.Collection_contains_null_element, "keys")); } object value = GetInternal(key, null); if (value != null) { if (values == null) { values = new Dictionary(); } values[key] = value; } } } return values; } // used when redirecting ASP.NET cache into the MemoryCache. This avoids infinite recursion // due to the fact that the (ASP.NET) config system uses the cache, and the cache uses the // config system. [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Grandfathered suppression from original caching code checkin")] internal void UpdateConfig(NameValueCollection config) { if (config == null) { throw new ArgumentNullException("config"); } if (!IsDisposed) { _stats.UpdateConfig(config); } } } }