csharp/acarteas/FileCache/src/FileCache/FileCache.cs

FileCache.cs
/*
3Copyright 2012, 2013, 2017 Adam Carter (http://adam-carter.com)

This file is part of FileCache (http://github.com/acarteas/FileCache).

FileCache is distributed under the Apache License 2.0.
Consult "LICENSE.txt" included in this package for the Apache License 2.0.
*/
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using System.Threading.Tasks;

namespace System.Runtime.Caching
{
    public clast FileCache : ObjectCache
    {
        private static int _nameCounter = 1;
        private string _name = "";
        private SerializationBinder _binder;
        private static FileCacheManagers _defaultManager = FileCacheManagers.Basic;
        private string _cacheSubFolder = "cache";
        private string _policySubFolder = "policy";
        private TimeSpan _cleanInterval = new TimeSpan(0, 0, 0, 0); // default to 1 week
        private const string LastCleanedDateFile = "cache.lcd";
        private const string CacheSizeFile = "cache.size";
        // this is a file used to prevent multiple processes from trying to "clean" at the same time
        private const string SemapreplacedFile = "cache.sem";
        private long _currentCacheSize = 0;
        public string CacheDir { get; protected set; }

        /// 
        /// Allows for the setting of the default cache manager so that it doesn't have to be
        /// specified on every instance creation.
        /// 
        public static FileCacheManagers DefaultCacheManager
        {
            get
            {
                return _defaultManager;
            }
            set
            {
                _defaultManager = value;
            }
        }

        /// 
        /// Used to abstract away the low-level details of file management.  This allows
        /// for multiple file formatting schemes based on use case.
        /// 
        public FileCacheManager CacheManager { get; protected set; }

        /// 
        /// Used to store the default region when accessing the cache via [] calls
        /// 
        public string DefaultRegion { get; set; }

        /// 
        /// Used to set the default policy when setting cache values via [] calls
        /// 
        public CacheItemPolicy DefaultPolicy { get; set; }

        /// 
        /// Specified how the cache payload is to be handled.
        /// 
        public enum PayloadMode
        {
            /// 
            /// Treat the payload a a serializable object.
            /// 
            Serializable,
            /// 
            /// Treat the payload as a file name. File content will be copied on add, while get returns the file name.
            /// 
            Filename,
            /// 
            /// Treat the paylad as raw bytes. A byte[] and readable streams are supported on add.
            /// 
            RawBytes
        }

        /// 
        /// Specified whether the payload is deserialized or just the file name.
        /// 
        public PayloadMode PayloadReadMode { get; set; } = PayloadMode.Serializable;

        /// 
        /// Specified how the payload is to be handled on add operations.
        /// 
        public PayloadMode PayloadWriteMode { get; set; } = PayloadMode.Serializable;

        /// 
        /// The amount of time before expiry that a filename will be used as a payoad. I.e.
        /// the amount of time the cache's user can safely use the file delivered as a payload.
        /// Default 10 minutes.
        /// 
        public TimeSpan FilenameAsPayloadSafetyMargin = TimeSpan.FromMinutes(10);

        /// 
        /// Used to determine how long the FileCache will wait for a file to become
        /// available.  Default (00:00:00) is indefinite.  Should the timeout be
        /// reached, an exception will be thrown.
        /// 
        public TimeSpan AccessTimeout
        {
            get
            {
                return CacheManager.AccessTimeout;
            }
            set
            {
                CacheManager.AccessTimeout = value;
            }
        }

        /// 
        /// Used to specify the disk size, in bytes, that can be used by the File Cache
        /// 
        public long MaxCacheSize { get; set; }

        /// 
        /// Returns the approximate size of the file cache
        /// 
        public long CurrentCacheSize
        {
            get
            {
                // if this is the first query, we need to load the cache size from somewhere
                if (_currentCacheSize == 0)
                {
                    // Read the system file for cache size
                    long cacheSize;
                    if (CacheManager.ReadSysValue(CacheSizeFile, out cacheSize))
                    {
                        // Did we successfully get data from the file? Write it to our member var.
                        _currentCacheSize = cacheSize;
                    }
                }

                return _currentCacheSize;
            }
            private set
            {
                // no need to do a pointless re-store of the same value
                if (_currentCacheSize != value || value == 0)
                {
                    CacheManager.WriteSysValue(CacheSizeFile, value);
                    _currentCacheSize = value;
                }
            }
        }

        /// 
        /// Event that will be called when  is reached.
        /// 
        public event EventHandler MaxCacheSizeReached = delegate { };

        public event EventHandler CacheResized = delegate { };

        /// 
        /// The default cache path used by FC.
        /// 
        private string DefaultCachePath
        {
            get
            {
                return Path.Combine(Directory.GetCurrentDirectory(), "FileCache");
            }
        }

        #region constructors

        /// 
        /// Creates a default instance of the file cache using the supplied file cache manager.
        /// 
        /// 
        public FileCache(FileCacheManagers manager)
        {
            Init(manager, false, new TimeSpan(), true, true);
        }

        /// 
        /// Creates a default instance of the file cache.  Don't use if you plan to serialize custom objects
        /// 
        /// If true, will calcualte the cache's current size upon new object creation.
        /// Turned off by default as directory traversal is somewhat expensive and may not always be necessary based on
        /// use case.
        /// 
        /// If supplied, sets the interval of time that must occur between self cleans
        public FileCache(
            bool calculateCacheSize = false,
            TimeSpan cleanInterval = new TimeSpan()
            )
        {
            // CT note: I moved this code to an init method because if the user specified a cache root, that needs to
            // be set before checking if we should clean (otherwise it will look for the file in the wrong place)
            Init(DefaultCacheManager, calculateCacheSize, cleanInterval, true, true);
        }

        /// 
        /// Creates an instance of the file cache using the supplied path as the root save path.
        /// 
        /// The cache's root file path
        /// If true, will calcualte the cache's current size upon new object creation.
        /// Turned off by default as directory traversal is somewhat expensive and may not always be necessary based on
        /// use case.
        /// 
        /// If supplied, sets the interval of time that must occur between self cleans
        public FileCache(
            string cacheRoot,
            bool calculateCacheSize = false,
            TimeSpan cleanInterval = new TimeSpan())
        {
            CacheDir = cacheRoot;
            Init(DefaultCacheManager, calculateCacheSize, cleanInterval, false, true);
        }

        /// 
        /// Creates an instance of the file cache.
        /// 
        /// The SerializationBinder used to deserialize cached objects.  Needed if you plan
        /// to cache custom objects.
        /// 
        /// If true, will calcualte the cache's current size upon new object creation.
        /// Turned off by default as directory traversal is somewhat expensive and may not always be necessary based on
        /// use case.
        /// 
        /// If supplied, sets the interval of time that must occur between self cleans
        public FileCache(
            SerializationBinder binder,
            bool calculateCacheSize = false,
            TimeSpan cleanInterval = new TimeSpan()
            )
        {
            _binder = binder;
            Init(DefaultCacheManager, calculateCacheSize, cleanInterval, true, false);
        }

        /// 
        /// Creates an instance of the file cache.
        /// 
        /// The cache's root file path
        /// The SerializationBinder used to deserialize cached objects.  Needed if you plan
        /// to cache custom objects.
        /// If true, will calcualte the cache's current size upon new object creation.
        /// Turned off by default as directory traversal is somewhat expensive and may not always be necessary based on
        /// use case.
        /// 
        /// If supplied, sets the interval of time that must occur between self cleans
        public FileCache(
            string cacheRoot,
            SerializationBinder binder,
            bool calculateCacheSize = false,
            TimeSpan cleanInterval = new TimeSpan()
            )
        {
            _binder = binder;
            CacheDir = cacheRoot;
            Init(DefaultCacheManager, calculateCacheSize, cleanInterval, false, false);
        }

        /// 
        /// Creates an instance of the file cache.
        /// 
        /// 
        /// The cache's root file path
        /// The SerializationBinder used to deserialize cached objects.  Needed if you plan
        /// to cache custom objects.
        /// If true, will calcualte the cache's current size upon new object creation.
        /// Turned off by default as directory traversal is somewhat expensive and may not always be necessary based on
        /// use case.
        /// 
        /// If supplied, sets the interval of time that must occur between self cleans
        public FileCache(
            FileCacheManagers manager,
            string cacheRoot,
            SerializationBinder binder,
            bool calculateCacheSize = false,
            TimeSpan cleanInterval = new TimeSpan()
        )
        {
            _binder = binder;
            CacheDir = cacheRoot;
            Init(manager, calculateCacheSize, cleanInterval, false, false);
        }

        #endregion

        #region custom methods

        private void Init(
            FileCacheManagers manager,
            bool calculateCacheSize = false,
            TimeSpan cleanInterval = new TimeSpan(),
            bool setCacheDirToDefault = true,
            bool setBinderToDefault = true
            )
        {
            _name = "FileCache_" + _nameCounter;
            _nameCounter++;

            DefaultRegion = null;
            DefaultPolicy = new CacheItemPolicy();
            MaxCacheSize = long.MaxValue;

            // set default values if not already set
            if (setCacheDirToDefault)
            {
                this.CacheDir = this.DefaultCachePath;
            }
            if (setBinderToDefault)
            {
                this._binder = new FileCacheBinder();
            }

            // if it doesn't exist, we need to make it
            if (!Directory.Exists(CacheDir))
            {
                Directory.CreateDirectory(this.CacheDir);
            }

            // only set the clean interval if the user supplied it
            if (cleanInterval > new TimeSpan())
            {
                _cleanInterval = cleanInterval;
            }

            //set up cache manager
            CacheManager = FileCacheManagerFactory.Create(manager);
            CacheManager.CacheDir = CacheDir;
            CacheManager.CacheSubFolder = _cacheSubFolder;
            CacheManager.PolicySubFolder = _policySubFolder;
            CacheManager.Binder = _binder;
            CacheManager.AccessTimeout = new TimeSpan();

            //check to see if cache is in need of immediate cleaning
            if (ShouldClean())
            {
                CleanCacheAsync();
            }
            else if (calculateCacheSize || CurrentCacheSize == 0)
            {
                // This is in an else if block, because CleanCacheAsync will
                // update the cache size, so no need to do it twice.
                UpdateCacheSizeAsync();
            }

            MaxCacheSizeReached += FileCache_MaxCacheSizeReached;
        }

        private void FileCache_MaxCacheSizeReached(object sender, FileCacheEventArgs e)
        {
            Task.Factory.StartNew((Action) (() =>
            {
                // Shrink the cache to 75% of the max size
                // that way there's room for it to grow a bit
                // before we have to do this again.
                long newSize = ShrinkCacheToSize((long)(MaxCacheSize*0.75));
            }));
        }


        // Returns the cleanlock file if it can be opened, otherwise it is being used by another process so return null
        private FileStream GetCleaningLock()
        {
            try
            {
                return File.Open(Path.Combine(CacheDir, SemapreplacedFile), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
            }
            catch (Exception)
            {
                return null;
            }
        }

        // Determines whether or not enough time has pasted that the cache should clean itself
        private bool ShouldClean()
        {
            try
            {
                // if the file can't be found, or is corrupt this will throw an exception
                DateTime lastClean;
                if (!CacheManager.ReadSysValue(LastCleanedDateFile, out lastClean))
                {
                    //AC: rewrote to be safer in cases where no value obtained.
                    return true;
                }

                // return true if the amount of time between now and the last clean is greater than or equal to the
                // clean interval, otherwise return false.
                return DateTime.Now - lastClean >= _cleanInterval;
            }
            catch (Exception)
            {
                return true;
            }
        }

        /// 
        /// Shrinks the cache until the cache size is less than
        /// or equal to the size specified (in bytes). This is a
        /// rather expensive operation, so use with discretion.
        /// 
        /// The new size of the cache
        public long ShrinkCacheToSize(long newSize, string regionName = null)
        {
            long originalSize = 0, amount = 0, removed = 0;

            //lock down other treads from trying to shrink or clean
            using (FileStream cLock = GetCleaningLock())
            {
                if (cLock == null)
                {
                    return -1;
                }

                // if we're shrinking the whole cache, we can use the stored
                // size if it's available. If it's not available we calculate it and store
                // it for next time.
                if (regionName == null)
                {
                    if (CurrentCacheSize == 0)
                    {
                        CurrentCacheSize = GetCacheSize();
                    }

                    originalSize = CurrentCacheSize;
                }
                else
                {
                    originalSize = GetCacheSize(regionName);
                }

                // Find out how much we need to get rid of
                amount = originalSize - newSize;

                // CT note: This will update CurrentCacheSize
                removed = DeleteOldestFiles(amount, regionName);

                // unlock the semapreplaced for others
                cLock.Close();
            }

            // trigger the event
            CacheResized(this, new FileCacheEventArgs(originalSize - removed, MaxCacheSize));

            // return the final size of the cache (or region)
            return originalSize - removed;
        }

        public void CleanCacheAsync()
        {
            Task.Factory.StartNew((Action) (() =>
            {
                CleanCache();
            }));
        }

        /// 
        /// Loop through the cache and delete all expired files
        /// 
        /// The amount removed (in bytes)
        public long CleanCache(string regionName = null)
        {
            long removed = 0;

            //lock down other treads from trying to shrink or clean
            using (FileStream cLock = GetCleaningLock())
            {
                if (cLock == null)
                    return 0;

                IEnumerable regions =
                    string.IsNullOrEmpty(regionName)
                        ? CacheManager.GetRegions()
                        : new List(1) { regionName };

                foreach (var region in regions)
                {
                    foreach (string key in GetKeys(region))
                    {
                        CacheItemPolicy policy = GetPolicy(key, region);
                        if (policy.AbsoluteExpiration < DateTime.Now)
                        {
                            try
                            {
                                string cachePath = CacheManager.GetCachePath(key, region);
                                string policyPath = CacheManager.GetPolicyPath(key, region);
                                CacheItemReference ci = new CacheItemReference(key, region, cachePath, policyPath);
                                Remove(key, region); // CT note: Remove will update CurrentCacheSize
                                removed += ci.Length;
                            }
                            catch (Exception) // skip if the file cannot be accessed
                            { }
                        }
                    }
                }

                // mark that we've cleaned the cache
                CacheManager.WriteSysValue(LastCleanedDateFile, DateTime.Now);

                // unlock
                cLock.Close();
            }

            return removed;
        }

        /// 
        /// Delete the oldest items in the cache to shrink the chache by the
        /// specified amount (in bytes).
        /// 
        /// The amount of data that was actually removed
        private long DeleteOldestFiles(long amount, string regionName = null)
        {
            // Verify that we actually need to shrink
            if (amount  0)
            {
                //remove oldest item
                CacheItemReference oldest = cacheReferences.Dequeue();
                removedBytes += oldest.Length;
                Remove(oldest.Key, oldest.Region);
            }
            return removedBytes;
        }

        /// 
        /// This method calls GetCacheSize on a separate thread to
        /// calculate and then store the size of the cache.
        /// 
        public void UpdateCacheSizeAsync()
        {
            Task.Factory.StartNew((Action) (() =>
            {
                CurrentCacheSize = GetCacheSize();
            }));
        }

        //AC Note: From MSDN / SO (http://stackoverflow.com/questions/468119/whats-the-best-way-to-calculate-the-size-of-a-directory-in-net)
        /// 
        /// Calculates the size, in bytes of the file cache
        /// 
        /// The region to calculate.  If NULL, will return total size.
        /// 
        public long GetCacheSize(string regionName = null)
        {
            long size = 0;

            //AC note: First parameter is unused, so just past in garbage ("DummyValue")
            string policyPath = Path.GetDirectoryName(CacheManager.GetPolicyPath("DummyValue", regionName));
            string cachePath = Path.GetDirectoryName(CacheManager.GetCachePath("DummyValue", regionName));
            size += CacheSizeHelper(new DirectoryInfo(policyPath));
            size += CacheSizeHelper(new DirectoryInfo(cachePath));
            return size;
        }

        /// 
        /// Helper method for public .
        /// 
        /// 
        /// 
        private long CacheSizeHelper(DirectoryInfo root)
        {
            long size = 0;

            // Add file sizes.
            var fis = root.EnumerateFiles();
            foreach (FileInfo fi in fis)
            {
                size += fi.Length;
            }

            // Add subdirectory sizes.
            var dis = root.EnumerateDirectories();
            foreach (DirectoryInfo di in dis)
            {
                size += CacheSizeHelper(di);
            }

            return size;
        }

        /// 
        /// Clears all FileCache-related items from the disk.  Throws an exception if the cache can't be
        /// deleted.
        /// 
        public void Clear()
        {
            //Before we can delete the entire file tree, we have to wait for any latent writes / reads to finish
            //To do this, we wait for access to our cacheLock file.  When we get access, we have to immediately
            //release it (can't delete a file that is open!), which somewhat muddies our condition of needing
            //exclusive access to the FileCache.  However, the time between closing and making the call to
            //delete is so small that we probably won't run into an exception most of the time.
            FileStream cacheLock = null;
            TimeSpan totalTime = new TimeSpan(0);
            TimeSpan interval = new TimeSpan(0, 0, 0, 0, 50);
            TimeSpan timeToWait = AccessTimeout;
            if (AccessTimeout == new TimeSpan())
            {
                //if access timeout is not set, make really large wait time
                timeToWait = new TimeSpan(5, 0, 0);
            }
            while (cacheLock == null && timeToWait > totalTime)
            {
                cacheLock = GetCleaningLock();
                Thread.Sleep(interval);
                totalTime += interval;
            }
            if (cacheLock == null)
            {
                throw new TimeoutException("FileCache AccessTimeout reached when attempting to clear cache.");
            }
            cacheLock.Close();

            //now that we've waited for everything to stop, we can delete the cache directory.
            Directory.Delete(CacheDir, true);
        }

        /// 
        /// Flushes the file cache using DateTime.Now as the minimum date
        /// 
        /// 
        public void Flush(string regionName = null)
        {
            Flush(DateTime.Now, regionName);
        }

        /// 
        /// Flushes the cache based on last access date, filtered by optional region
        /// 
        /// 
        /// 
        public void Flush(DateTime minDate, string regionName = null)
        {
            // prevent other threads from altering stuff while we delete junk
            using (FileStream cLock = GetCleaningLock())
            {
                if (cLock == null)
                {
                    return;
                }

                IEnumerable regions =
                    string.IsNullOrEmpty(regionName)
                        ? CacheManager.GetRegions()
                        : new List(1) { regionName };

                foreach (var region in regions)
                {
                    IEnumerable keys = CacheManager.GetKeys(region);
                    foreach (string key in keys)
                    {
                        string policyPath = CacheManager.GetPolicyPath(key, region);
                        string cachePath = CacheManager.GetCachePath(key, region);

                        // Update the Cache size before flushing this item.
                        CurrentCacheSize = GetCacheSize();

                        //if either policy or cache are stale, delete both
                        if (File.GetLastAccessTime(policyPath) < minDate || File.GetLastAccessTime(cachePath) < minDate)
                        {
                            CurrentCacheSize -= CacheManager.DeleteFile(key, region);
                        }
                    }
                }

                // unlock
                cLock.Close();
            }
        }

        /// 
        /// Returns the policy attached to a given cache item.
        /// 
        /// The key of the item
        /// The region in which the key exists
        /// 
        public CacheItemPolicy GetPolicy(string key, string regionName = null)
        {
            CacheItemPolicy policy = new CacheItemPolicy();
            FileCachePayload payload = CacheManager.ReadFile(PayloadMode.Filename, key, regionName) as FileCachePayload;
            if (payload != null)
            {
                try
                {
                    policy.SlidingExpiration = payload.Policy.SlidingExpiration;
                    policy.AbsoluteExpiration = payload.Policy.AbsoluteExpiration;
                }
                catch (Exception)
                {
                }
            }
            return policy;
        }

        public IEnumerable GetKeys(string regionName = null)
        {
            return CacheManager.GetKeys(regionName);
        }

        #endregion

        #region private helpers

        private void WriteHelper(PayloadMode mode, string key, FileCachePayload data, string regionName = null, bool policyUpdateOnly = false)
        {
            CurrentCacheSize += CacheManager.WriteFile(mode, key, data, regionName, policyUpdateOnly);

            //check to see if limit was reached
            if (CurrentCacheSize > MaxCacheSize)
            {
                MaxCacheSizeReached(this, new FileCacheEventArgs(CurrentCacheSize, MaxCacheSize));
            }
        }

        #endregion

        #region ObjectCache overrides

        public override object AddOrGetExisting(string key, object value, CacheItemPolicy policy, string regionName = null)
        {
            string path = CacheManager.GetCachePath(key, regionName);
            object oldData = null;

            //pull old value if it exists
            if (File.Exists(path))
            {
                try
                {
                    oldData = Get(key, regionName);
                }
                catch (Exception)
                {
                    oldData = null;
                }
            }
            SerializableCacheItemPolicy cachePolicy = new SerializableCacheItemPolicy(policy);
            FileCachePayload newPayload = new FileCachePayload(value, cachePolicy);
            WriteHelper(PayloadWriteMode, key, newPayload, regionName);

            //As docameented in the spec (http://msdn.microsoft.com/en-us/library/dd780602.aspx), return the old
            //cached value or null
            return oldData;
        }

        public override CacheItem AddOrGetExisting(CacheItem value, CacheItemPolicy policy)
        {
            object oldData = AddOrGetExisting(value.Key, value.Value, policy, value.RegionName);
            CacheItem returnItem = null;
            if (oldData != null)
            {
                returnItem = new CacheItem(value.Key)
                {
                    Value = oldData,
                    RegionName = value.RegionName
                };
            }
            return returnItem;
        }

        public override object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
        {
            CacheItemPolicy policy = new CacheItemPolicy();
            policy.AbsoluteExpiration = absoluteExpiration;
            return AddOrGetExisting(key, value, policy, regionName);
        }

        public override bool Contains(string key, string regionName = null)
        {
            string path = CacheManager.GetCachePath(key, regionName);
            return File.Exists(path);
        }

        public override CacheEntryChangeMonitor CreateCacheEntryChangeMonitor(IEnumerable keys, string regionName = null)
        {
            throw new NotImplementedException();
        }

        public override DefaultCacheCapabilities DefaultCacheCapabilities
        {
            get
            {
                //AC note: can use boolean OR "|" to set multiple flags.
                return DefaultCacheCapabilities.CacheRegions
                    |
                    DefaultCacheCapabilities.AbsoluteExpirations
                    |
                    DefaultCacheCapabilities.SlidingExpirations
                    ;
            }
        }

        public override object Get(string key, string regionName = null)
        {
            FileCachePayload payload = CacheManager.ReadFile(PayloadReadMode, key, regionName) as FileCachePayload;
            string cachedItemPath = CacheManager.GetCachePath(key, regionName);

            DateTime cutoff = DateTime.Now;
            if (PayloadReadMode == PayloadMode.Filename)
            {
                cutoff += FilenameAsPayloadSafetyMargin;
            }

            //null payload?
            if (payload.Policy != null && payload.Payload != null)
            {
                //did the item expire?
                if (payload.Policy.AbsoluteExpiration < cutoff)
                {
                    //set the payload to null
                    payload.Payload = null;

                    //delete the file from the cache
                    try
                    {
                        // CT Note: I changed this to Remove from File.Delete so that the coresponding
                        // policy file will be deleted as well, and CurrentCacheSize will be updated.
                        Remove(key, regionName);
                    }
                    catch (Exception)
                    {
                    }
                }
                else
                {
                    //does the item have a sliding expiration?
                    if (payload.Policy.SlidingExpiration > new TimeSpan())
                    {
                        payload.Policy.AbsoluteExpiration = DateTime.Now.Add(payload.Policy.SlidingExpiration);
                        WriteHelper(PayloadWriteMode, key, payload, regionName, true);
                    }

                }
            }
            else
            {
                //remove null payload
                Remove(key, regionName);

                //create dummy one for return
                payload = new FileCachePayload(null);
            }
            return payload.Payload;
        }

        public override CacheItem GetCacheItem(string key, string regionName = null)
        {
            object value = Get(key, regionName);
            CacheItem item = new CacheItem(key);
            item.Value = value;
            item.RegionName = regionName;
            return item;
        }

        public override long GetCount(string regionName = null)
        {
            if (regionName == null)
            {
                regionName = "";
            }
            string path = Path.Combine(CacheDir, _cacheSubFolder, regionName);
            if (Directory.Exists(path))
                return Directory.GetFiles(path).Count();
            else
                return 0;
        }

        /// 
        /// Returns an enumerator for the specified region (defaults to base-level cache directory).
        /// This function *WILL NOT* recursively locate files in subdirectories.
        /// 
        /// 
        /// 
        public IEnumerator GetEnumerator(string regionName = null)
        {
            string region = "";
            if (string.IsNullOrEmpty(regionName) == false)
            {
                region = regionName;
            }

            //AC: This seems inefficient.  Wouldn't it be better to do this using a cursor?
            List enumerator = new List();

            var keys = CacheManager.GetKeys(regionName);
            foreach (string key in keys)
            {
                enumerator.Add(new KeyValuePair(key, this.Get(key, regionName)));
            }
            return enumerator.GetEnumerator();
        }

        /// 
        /// Will return an enumerator with all cache items listed in the root file path ONLY.  Use the other
        ///  if you want to specify a region
        /// 
        /// 
        protected override IEnumerator GetEnumerator()
        {
            return GetEnumerator(null);
        }

        public override IDictionary GetValues(IEnumerable keys, string regionName = null)
        {
            Dictionary values = new Dictionary();
            foreach (string key in keys)
            {
                values[key] = Get(key, regionName);
            }
            return values;
        }

        public override string Name
        {
            get { return _name; }
        }

        public override object Remove(string key, string regionName = null)
        {
            object valueToDelete = null;

            if (Contains(key, regionName) == true)
            {

                // Because of the possibility of multiple threads accessing this, it's possible that
                // while we're trying to remove something, another thread has already removed it.
                try
                {
                    //remove cache entry
                    // CT note: calling Get from remove leads to an infinite loop and stack overflow,
                    // so I replaced it with a simple CacheManager.ReadFile call. None of the code here actually
                    // uses this object returned, but just in case someone else's outside code does.
                    FileCachePayload fcp = CacheManager.ReadFile(PayloadMode.Filename, key, regionName);
                    valueToDelete = fcp.Payload;
                    string path = CacheManager.GetCachePath(key, regionName);
                    CurrentCacheSize -= new FileInfo(path).Length;
                    File.Delete(path);

                    //remove policy file
                    string cachedPolicy = CacheManager.GetPolicyPath(key, regionName);
                    CurrentCacheSize -= new FileInfo(cachedPolicy).Length;
                    File.Delete(cachedPolicy);
                }
                catch (IOException)
                {
                }

            }
            return valueToDelete;
        }

        public override void Set(string key, object value, CacheItemPolicy policy, string regionName = null)
        {
            Add(key, value, policy, regionName);
        }

        public override void Set(CacheItem item, CacheItemPolicy policy)
        {
            Add(item, policy);
        }

        public override void Set(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
        {
            Add(key, value, absoluteExpiration, regionName);
        }

        public override object this[string key]
        {
            get
            {
                return this.Get(key, DefaultRegion);
            }
            set
            {
                this.Set(key, value, DefaultPolicy, DefaultRegion);
            }
        }

        #endregion

        private clast LocalCacheBinder : System.Runtime.Serialization.SerializationBinder
        {
            public override Type BindToType(string astemblyName, string typeName)
            {
                Type typeToDeserialize = null;

                String currentastembly = astembly.Getastembly(typeof(LocalCacheBinder)).FullName;
                astemblyName = currentastembly;

                // Get the type using the typeName and astemblyName
                typeToDeserialize = Type.GetType(String.Format("{0}, {1}",
                    typeName, astemblyName));

                return typeToDeserialize;
            }
        }

        // CT: This private clast is used to help shrink the cache.
        // It computes the total size of an entry including it's policy file.
        // It also implements IComparable functionality to allow for sorting based on access time
        private clast CacheItemReference : IComparable
        {
            public readonly DateTime LastAccessTime;
            public readonly long Length;
            public readonly string Key;
            public readonly string Region;

            public CacheItemReference(string key, string region, string cachePath, string policyPath)
            {
                Key = key;
                Region = region;
                FileInfo cfi = new FileInfo(cachePath);
                FileInfo pfi = new FileInfo(policyPath);
                cfi.Refresh();
                LastAccessTime = cfi.LastAccessTime;
                Length = cfi.Length + pfi.Length;
            }

            public int CompareTo(CacheItemReference other)
            {
                int i = LastAccessTime.CompareTo(other.LastAccessTime);

                // It's possible, although rare, that two different items will have
                // the same LastAccessTime. So in that case, we need to check to see
                // if they're actually the same.
                if (i == 0)
                {
                    // second order should be length (but from smallest to largest,
                    // that way we delete smaller files first)
                    i = -1 * Length.CompareTo(other.Length);
                    if (i == 0)
                    {
                        i = string.Compare(Region, other.Region);
                        if (i == 0)
                        {
                            i = Key.CompareTo(other.Key);
                        }
                    }
                }

                return i;
            }

            public static bool operator >(CacheItemReference lhs, CacheItemReference rhs)
            {
                if(lhs.CompareTo(rhs) > 0)
                {
                    return true;
                }
                return false;
            }

            public static bool operator