File: System\IdentityModel\BoundedCache.cs
Project: ndp\cdf\src\WCF\IdentityModel\System.IdentityModel.csproj (System.IdentityModel)
//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------
 
using System.Collections.Generic;
using System.Threading;
 
namespace System.IdentityModel
{
    /// <summary>
    /// A cache of type T where items are cached and removed 
    /// according to the type specified, currently only 'TimeBounded' is supported.  An items is added with an expiration time.
    /// </summary>
    internal class BoundedCache<T> where T : class
    {
        Dictionary<string, ExpirableItem<T>> _items;
        int _capacity;
        TimeSpan _purgeInterval;
        ReaderWriterLock _readWriteLock;
        DateTime _nextPurgeTime = DateTime.UtcNow;
 
        /// <summary>
        /// Creates a cache for items of Type 'T' where expired items will purged on a regular interval
        /// </summary>
        /// <param name="capacity">The maximum size of the cache in number of items.
        /// If int.MaxValue is passed then the size is not bound.</param>
        /// <param name="purgeInterval">The time interval for checking expired items.</param>
        /// 
        public BoundedCache(int capacity, TimeSpan purgeInterval)
            : this(capacity, purgeInterval, StringComparer.Ordinal)
        { }
 
        /// <summary>
        /// Creates a cache for items of Type 'T' where expired items will purged on a regular interval
        /// </summary>
        /// <param name="capacity">The maximum size of the cache in number of items.
        /// If int.MaxValue is passed then the size is not bound.</param>
        /// <param name="purgeInterval">The time interval for checking expired items.</param>
        /// <param name="keyComparer">EqualityComparer for comparing keys.</param>
        /// <exception cref="ArgumentOutOfRangeException">The input parameter 'capacity' is less than or equal to zero.</exception>
        /// <exception cref="ArgumentOutOfRangeException">The input parameter 'purgeInterval' is less than or equal to TimeSpan.Zero.</exception>
        /// <exception cref="ArgumentNullException">The input parameter 'keyComparer' is null.</exception>
        public BoundedCache(int capacity, TimeSpan purgeInterval, IEqualityComparer<string> keyComparer)
        {
            if (capacity <= 0)
            {
                throw DiagnosticUtility.ThrowHelperArgumentOutOfRange("capacity", capacity, SR.GetString(SR.ID0002));
            }
 
            if (purgeInterval <= TimeSpan.Zero)
            {
                throw DiagnosticUtility.ThrowHelperArgumentOutOfRange("purgeInterval", purgeInterval, SR.GetString(SR.ID0016));
            }
 
            if (keyComparer == null)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("keyComparer");
            }
 
            _capacity = capacity;
            _purgeInterval = purgeInterval;
            _items = new Dictionary<string, ExpirableItem<T>>(keyComparer);
            _readWriteLock = new ReaderWriterLock();
        }
 
        /// <summary>
        /// Gets the ReaderWriterLock for controlling simultaneous reads and writes
        /// </summary>
        protected ReaderWriterLock CacheLock
        {
            get
            {
                return _readWriteLock;
            }
        }
 
        /// <summary>
        /// Gets or Sets the current Capacity of the cache in number of items.
        /// </summary>
        public virtual int Capacity
        {
            get
            {
                return _capacity;
            }
            set
            {
                if (value <= 0)
                {
                    throw DiagnosticUtility.ThrowHelperArgumentOutOfRange("value", value, SR.GetString(SR.ID0002));
                }
                _capacity = value;
            }
        }
 
        /// <summary>
        /// Removes all items from the Cache.
        /// </summary>      
        public virtual void Clear()
        {
            // -1 milleseconds is infinite timeout
            _readWriteLock.AcquireWriterLock(TimeSpan.FromMilliseconds(-1));
 
            try
            {
                _items.Clear();
            }
            finally
            {
                _readWriteLock.ReleaseWriterLock();
            }
        }
 
        /// <summary>
        /// Ensures that the maximum size is not exceeded
        /// </summary>
        /// <exception cref="LimitExceededException">If the Capacity of the cache has been reached.</exception>
        void EnforceQuota()
        {
            // int.MaxValue => unbounded
            if (_capacity == int.MaxValue)
            {
                return;
            }
 
            if (_items.Count >= _capacity)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new LimitExceededException(SR.GetString(SR.ID0021, _capacity)));
            }
        }
 
        /// <summary>
        /// Increases the maximum number of items that the cache will hold. 
        /// </summary>
        /// <param name="size">The capacity to increase.</param>
        /// <exception cref="ArgumentOutOfRangeException">The input parameter 'size' is less than or equal to zero.</exception>
        /// <returns>Updated capacity</returns>
        /// <remarks>If size + current capacity >= int.MaxValue then capacity will be set to int.MaxValue and the cache will be unbounded</remarks>
        public virtual int IncreaseCapacity(int size)
        {
            if (size <= 0)
            {
                throw DiagnosticUtility.ThrowHelperArgumentOutOfRange("size", size, SR.GetString(SR.ID0002));
            }
 
            // -1 milleseconds is infinite timeout
            _readWriteLock.AcquireWriterLock(TimeSpan.FromMilliseconds(-1));
 
            try
            {
                if (int.MaxValue - size <= _capacity)
                {
                    _capacity = int.MaxValue;
                }
                else
                {
                    _capacity = _capacity + size;
                }
 
                return _capacity;
            }
            finally
            {
                _readWriteLock.ReleaseWriterLock();
            }
        }
 
        /// <summary>
        /// Gets the Dictionary that contains the cached items.
        /// </summary>
        protected Dictionary<string, ExpirableItem<T>> Items
        {
            get
            {
                return _items;
            }
        }
 
        /// <summary>
        /// This method must not be called from within a read or writer lock as a deadlock will occur.
        /// Checks the time a decides if a cleanup needs to occur.
        /// </summary>
        void Purge()
        {
            DateTime currentTime = DateTime.UtcNow;
            if (currentTime < _nextPurgeTime)                
            {
                return;
            }
 
            _nextPurgeTime = DateTimeUtil.Add(currentTime, _purgeInterval);
 
            // -1 milleseconds is infinite timeout
            _readWriteLock.AcquireWriterLock(TimeSpan.FromMilliseconds(-1));
 
            try
            {
 
                List<string> expiredItems = new List<string>();
                foreach (string key in _items.Keys)
                {
                    if (_items[key].IsExpired())
                    {
                        expiredItems.Add(key);
                    }
                }
 
                for (int i = 0; i < expiredItems.Count; ++i)
                {
                    _items.Remove(expiredItems[i]);
                }
            }
            finally
            {
                _readWriteLock.ReleaseWriterLock();
            }
        }
 
        /// <summary>
        /// Gets or Sets the time interval that will be used for checking expired items.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">If 'value' is less than or equal to TimeSpan.Zero.</exception>
        public TimeSpan PurgeInterval
        {
            get { return _purgeInterval; }
            set
            {
                if (value <= TimeSpan.Zero)
                {
                    throw DiagnosticUtility.ThrowHelperArgumentOutOfRange("value", value, SR.GetString(SR.ID0016));
                }
 
                _purgeInterval = value;
            }
        }
 
        /// <summary>
        /// Attempt to add a item to the cache.
        /// </summary>
        /// <param name="key">Key to use when adding item</param>
        /// <param name="item">Item of type 'T' to add to cache</param>
        /// <param name="expirationTime">The expiration time of the entry.</param>
        /// <returns>true if item was added, false if item was not added</returns>
        /// <exception cref="LimitExceededException">Thrown if an attempt is made to add an item when the current 
        /// cache size is equal to the capacity</exception>
        public virtual bool TryAdd(string key, T item, DateTime expirationTime)
        {
            Purge();
 
            // -1 milleseconds is infinite timeout
            _readWriteLock.AcquireWriterLock(TimeSpan.FromMilliseconds(-1));
 
            EnforceQuota();
 
            try
            {
                if (_items.ContainsKey(key))
                {
                    return false;
                }
                else
                {
                    _items[key] = new ExpirableItem<T>(item, expirationTime);
                    return true;
                }
            }
            finally
            {
                _readWriteLock.ReleaseWriterLock();
            }
        }
 
        /// <summary>
        /// Attempts to find an item in the cache
        /// </summary>
        /// <param name="key">Item to search for.</param>
        /// <returns>true if item is in cache, false otherwise</returns>
        /// <remarks>Item may be expired and would be purged next cycle</remarks>
        public virtual bool TryFind(string key)
        {
            Purge();
 
            // -1 milleseconds is infinite timeout
            _readWriteLock.AcquireReaderLock(TimeSpan.FromMilliseconds(-1));
            try
            {
                if (_items.ContainsKey(key) && !_items[key].IsExpired())
                {
                    return true;
                }
 
                return false;
            }
            finally
            {
                _readWriteLock.ReleaseReaderLock();
            }
        }
 
        /// <summary>
        /// Attempt to get an item from the Cache
        /// </summary>
        /// <param name="key">Item to seach for</param>
        /// <param name="item">The object refernece that will be set the the retrivied item.</param>
        /// <returns>true if item is found, false otherwise</returns>
        /// <remarks>Item may be expired and would be purged next cycle</remarks>
        public virtual bool TryGet(string key, out T item)
        {
            Purge();
 
            item = null;
 
            // -1 milleseconds is infinite timeout
 
            _readWriteLock.AcquireReaderLock(TimeSpan.FromMilliseconds(-1));
            try
            {
                if (_items.ContainsKey(key))
                {
                    if (!_items[key].IsExpired())
                    {
                        item = _items[key].Item;
                        return true;
                    }
                }
 
                return false;
 
            }
            finally
            {
                _readWriteLock.ReleaseReaderLock();
            }
        }
 
        /// <summary>
        /// Attempts to remove an item from the Cache
        /// </summary>
        /// <param name="key">Item to remove</param>
        /// <returns>true if item was removed, false otherwise</returns>
        public virtual bool TryRemove(string key)
        {
            Purge();
 
            // -1 milleseconds is infinite timeout
 
            _readWriteLock.AcquireWriterLock(TimeSpan.FromMilliseconds(-1));
            try
            {
                if (!_items.ContainsKey(key))
                {
                    return false;
                }
 
                _items.Remove(key);
                return true;
            }
            finally
            {
                _readWriteLock.ReleaseWriterLock();
            }
        }
 
        /// <summary>
        /// Wrapper class for objects contained in BoundedCache.  Contains the obj 'T' and 
        /// </summary>
        /// <typeparam name="ET">Type of the item</typeparam>
        protected class ExpirableItem<ET>
        {
            DateTime _expirationTime;
            ET _item;
 
            public ExpirableItem(ET item, DateTime expirationTime)
            {
                _item = item;
                if (expirationTime.Kind != DateTimeKind.Utc)
                {
                    _expirationTime = DateTimeUtil.ToUniversalTime(expirationTime);
                }
                else
                {
                    _expirationTime = expirationTime;
                }
            }
 
            public bool IsExpired()
            {
                return (_expirationTime <= DateTime.UtcNow);
            }
 
            public ET Item
            {
                get { return _item; }
            }
        }
 
        [Flags]
        internal enum CachingMode
        {
            Time,
            MRU,
            FIFO
        }
    }
}