Click here to Skip to main content
15,885,985 members
Articles / Web Development / ASP.NET

Peer to Peer ASP.NET State Server

Rate me:
Please Sign up or sign in to vote.
4.99/5 (48 votes)
24 Aug 2009Apache19 min read 120.9K   3.4K   119  
A distributed implementation of the ASP.NET state service
/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

using System;
using System.Collections.Generic;
using System.Threading;

namespace P2PStateServer
{
    public delegate void SessionReadHandler(ISessionObject Session,object StateObject);

    /// <summary>
    /// Represents a thread-safe session state dictionary.
    /// </summary>
    class SessionDictionary
    {
        //ReaderWriterLock used while accessing dictionary
        #if NET20
            ReaderWriterLock rwl = new ReaderWriterLock();
        #else
            ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();
        #endif

         //This stores a list of all sessions sorted by expiration date -- very helpful to the session expiry scavenger.
        DateSortedDictionary<string,string> expiryList = new DateSortedDictionary<string, string>();

        const int DeadLockIterationCount = 2000; //Number of iterations to count before declaring a deadlock

        private Dictionary<string, ISessionObject> dict = new Dictionary<string, ISessionObject>();

        /// <summary>
        /// Adds a session into the dictionary
        /// </summary>
        /// <param name="Key">Session Key</param>
        /// <param name="Session">Session object</param>
        /// <param name="UpdateIfNotFound">Indicates whether session should be updated if the session was not found. If set to flase, this gives the caller a chance to query the network before trying again</param>
        /// <param name="LockedSessionInfo">Locked session information if session is locked</param>
        /// <returns>Result of Action</returns>
        public SessionActionResult Add(string Key, ISessionObject Session,bool UpdateIfNotFound, out SessionResponseInfo LockedSessionInfo)
        {

            //If an item with key already exists, return SessionActionResult.AlreadyExists
            //else add the item and return SessionActionResult.OK
            //Calls UpSert internally

            return UpSert(Key, Session, true, UpdateIfNotFound, out LockedSessionInfo); //Insert Item if it doesn't exist
        }


        /// <summary>
        /// Updates a session (adds a new one if it was not found)
        /// </summary>
        /// <param name="Key">Session Key</param>
        /// <param name="Session">Session object</param>
        /// <param name="UpdateIfNotFound">Indicates whether session should be updated if the session was not found. If set to flase, this gives the caller a chance to query the network before trying again</param>
        /// <param name="LockedSessionInfo">Locked session information if session is locked</param>
        /// <returns>Result of Action</returns>
        public SessionActionResult Update(string Key, ISessionObject Session, bool UpdateIfNotFound, out SessionResponseInfo LockedSessionInfo)
        {

            //If the item is locked and the new items lockCookie does not match, return SessionActionResult.Locked
            //else add the item and return SessionActionResult.OK
            //Calls UpSert internally

            return UpSert(Key, Session, false, UpdateIfNotFound, out LockedSessionInfo); //Insert or Update item
        }


        /// <summary>
        /// Updates or inserts a session in the dictionary
        /// </summary>
        /// <param name="Key">Session Key</param>
        /// <param name="Session">Session object</param>
        /// <param name="InsertOnly">Indicates that session should only be inserted if it does not already exist</param>
        /// <param name="UpdateIfNotFound">Indicates whether session should be updated if the session was not found. If set to flase, this gives the caller a chance to query the network before trying again </param>
        /// <param name="LockedSessionInfo">Locked session information if session is locked</param>
        /// <returns>Result of Action</returns>
        private SessionActionResult UpSert(string Key, ISessionObject Session, bool InsertOnly, bool UpdateIfNotFound, out SessionResponseInfo LockedSessionInfo)
        {

            // Look for the session using a reader lock. 
            // If session is not found, switch to a writer lock and insert item.
            // If session is found:
            // Perform an atomic compare exchange on the variable 'InUse'
            // If session is in Use, try Upsert again from the start.
            // If session is not in Use, Perform UpSert and reset InUse
            // Also update Sorted session list

            if(Key == null) throw new ArgumentNullException("Key");
            if(Session == null) throw new ArgumentNullException("Session");

            LockedSessionInfo = null;
            bool tryAgain;

            Diags.ResetDeadLockCounter();
            do
            {
                tryAgain = false;
                AcquireReadLock();
                ISessionObject entry;
                try
                {
                    dict.TryGetValue(Key, out entry);
                }
                finally
                {
                    ReleaseReadLock();
                }

                if (entry == null)
                {
                    if (!UpdateIfNotFound)
                    {
                        return SessionActionResult.NotFound;
                    }
                    
                    //Session not found -- insert brand new session object
                    AcquireWriteLock();
                    try
                    {
                        //Check again to be sure now that the write-lock has been obtained
                        dict.TryGetValue(Key, out entry);
                        if (entry != null)
                        {
                            //ooops -- another thread inserted a seesion with this key while this thread was trying to obtain the write-lock
                            //so try again
                            tryAgain = true;
                            continue;
                        }

                        Session.LockCookie = 1; //For some reason Lockcookie starts counting from 2 -- so set it to 1 now so that it increments to 2 when sought
                        dict[Key] = Session;
                        expiryList.Add(DateTime.UtcNow.Add(new TimeSpan(0, Session.TimeOut,0)) , Key, Key);
                        Diags.LogNewSession(Key, Session);
                    }
                    finally
                    {
                        ReleaseWriteLock();
                    }


                }
                else
                {
                    //Session Found

                    if (InsertOnly)
                    {
                        Diags.LogSessionAlreadyExists(Key);
                        return SessionActionResult.AlreadyExists; //Do not perform an update if InsertOnly is requested
                    }

                    //There is no need to acquire a write lock here since the dictionary is not been modified. 
                    //Only the dictionary entry itself is been updated and such updates are guaranteed to be atomic 
                    //if the atomic InUse property is set.

                    if (entry.CompareExchangeIsInUse(true, false) == false)
                    {
                        //the InUse flag is set, so this code section has exclusive access to this session object
                        try
                        {
                            if (entry.IsLocked)
                            {
                                if (!entry.UnLock(Session.LockCookie ))
                                {
                                    //Lockcookie did not match
                                    LockedSessionInfo = (SessionResponseInfo) entry.CreateResponseInfo();
                                    Diags.LogSessionIsLocked(Key);
                                    return SessionActionResult.Locked;
                                }
                            }

                            Session.LockCookie = entry.LockCookie; //Overwrite the incoming session's lock-cookie with the internal one's so as not to let external input change the lockcookie 
                            Session.ExtraFlags = -1; //disable extra flags since an update is being performed

                            entry.CopyFrom(Session); //Copy all information from Session to entry
                            expiryList.Add(DateTime.UtcNow.Add(new TimeSpan(0, Session.TimeOut, 0)), Key, Key); //reset expiry timeout
                            Diags.LogUpdatedSession(Key, Session);
                        }
                        finally
                        {
                            entry.CompareExchangeIsInUse(false, true);
                        }

                    }
                    else
                    {
                        //Is this entry being exported?
                        if (entry.IsExporting)
                        {
                            //This session is already been exported so leave
                            Diags.ResetDeadLockCounter();
                            return SessionActionResult.Exporting;
                        }


                        //Another thread is using this session and will be done soon so try again
                        
                        Thread.Sleep(1); //pause for 1 ms
                        tryAgain = true;

                    }
                }

                Diags.DetectDeadLock(Key, DeadLockIterationCount); //Signal a deadlock after 2000 iterations

            } while (tryAgain);

            Diags.ResetDeadLockCounter(); //Signal deadlock was freed

            return SessionActionResult.OK;
        }

        /// <summary>
        /// Removes a session from the dictionary
        /// </summary>
        /// <param name="Key">Session Key</param>
        /// <param name="LockCookie">Lock Cookie (used to unlock item if it's locked)</param>
        /// <param name="LockedSessionInfo">Locked session information if session is locked</param>
        /// <returns>Result of Action</returns>
        public SessionActionResult Remove(string Key, uint LockCookie, out SessionResponseInfo LockedSessionInfo)
        {
            // Look for the session using a reader lock. 
            // If session is not found, return false;
            // If session is found:
            // Perform an atomic compare exchange on the variable 'InUse'
            // if session is in Use, try Delete again from the start.
            // if session is not In Use, Perform Delete

            return Remove(Key, LockCookie, false, DateTime.MinValue, out LockedSessionInfo);
        }

        /// <summary>
        /// Remove a session item because it has expired
        /// </summary>
        /// <param name="Key">Session Key</param>
        /// <param name="ExpiryDate">session Expiry date</param>
        /// <returns>Result of Action</returns>
        private SessionActionResult Expire(string Key, DateTime ExpiryDate)
        {
            SessionResponseInfo sessInfo;
            return Remove(Key, 0, true, ExpiryDate, out sessInfo) ;
        }

        /// <summary>
        /// Removes a session from the dictionary
        /// </summary>
        /// <param name="Key">Session Key</param>
        /// <param name="LockCookie">Lock Cookie</param>
        /// <param name="IsExpiring">Indicates that the item is being removed because it's expiring</param>
        /// <param name="ExpiryDate">The Item expiry date (for comparison)</param>
        /// <param name="LockedSessionInfo">Locked session information if session is locked</param>
        /// <returns>Result of Action</returns>
        private SessionActionResult Remove(string Key, uint LockCookie, bool IsExpiring, DateTime ExpiryDate ,out SessionResponseInfo LockedSessionInfo)
        {
            if (Key == null) throw new ArgumentNullException("Key");
            LockedSessionInfo = null;

            bool tryAgain;

            Diags.ResetDeadLockCounter();

            do
            {
                tryAgain = false;
                AcquireReadLock();
                ISessionObject entry;
                try
                {
                    dict.TryGetValue(Key, out entry);
                }
                finally
                {
                    ReleaseReadLock();
                }

                if (entry == null)
                {
                    //Session not found
                    Diags.LogSessionNotFound(Key);
                    return SessionActionResult.NotFound;
                }
                else
                {
                    //Session Found
                    if (entry.CompareExchangeIsInUse(true, false) == false)
                    {
                        try
                        {
                            //The InUse flag is set and so this code section has exclusive access to this session object
                            AcquireWriteLock();

                            try
                            {

                                //Check again to be sure, now that the write-lock has been obtained
                                ISessionObject oldEntry = entry;
                                if (!dict.TryGetValue(Key, out entry))
                                {
                                    //ooops -- another thread deleted the session from the dictionary while this thread 
                                    //was either trying to do the compareExchange (or if buggy, while obtaining the write-lock)
                                    //so try again
                                    oldEntry.CompareExchangeIsInUse(false, true); //unlock the previously locked item
                                    tryAgain = true;
                                    continue;
                                }

                                if (IsExpiring)
                                {
                                    DateTime timeStamp;
                                    if (expiryList.TryGetTimeStamp(Key, out timeStamp))
                                    {
                                        if (timeStamp != ExpiryDate)
                                        {
                                            //The expiration date on this session was updated, so leave
                                            return SessionActionResult.OK;
                                        }
                                    }
                                }

                                if (!IsExpiring && entry.IsLocked) //Locked items DO expire. if not expiring, LockCookie has to match session's 
                                {
                                    if (!entry.UnLock(LockCookie))
                                    {
                                        //Lockcookie did not match
                                        LockedSessionInfo = (SessionResponseInfo)entry.CreateResponseInfo();
                                        Diags.LogSessionIsLocked(Key);
                                        return SessionActionResult.Locked;
                                    }
                                }

                                if (dict.Remove(Key))
                                {
                                    expiryList.Remove(Key);
                                    if (IsExpiring)
                                    {
                                        Diags.LogSessionExpired(Key);
                                    }
                                    else
                                    {
                                        Diags.LogSessionDeleted(Key);
                                    }
                                }
                                else
                                {
                                    //This should never happen
                                    Diags.Fail("ASSERTION Failed -- Session dictionary was unable to remove key\r\n");
                                }

                            }
                            finally
                            {
                                ReleaseWriteLock();

                            }
                        }
                        finally
                        {
                            if (entry != null) entry.CompareExchangeIsInUse(false, true);
                        }
                    }
                    else
                    {
                        //Is this entry being exported?
                        if (entry.IsExporting)
                        {
                            //This session is already been exported so leave
                            Diags.ResetDeadLockCounter();
                            return SessionActionResult.Exporting;
                        }

                        //Another thread is using this session and will be done soon so try again

                        Thread.Sleep(1); //pause for 1 ms
                        tryAgain = true;


                    }

                    Diags.DetectDeadLock(Key, DeadLockIterationCount); //Signal a deadlock after 2000 iterations


                }
            } while (tryAgain);

            Diags.ResetDeadLockCounter(); //Signal deadlock was freed
            return SessionActionResult.OK;

        }


        /// <summary>
        /// Reads a stored session
        /// </summary>
        /// <param name="Key">Session Key</param>
        /// <param name="Reader">Method to call to complete read</param>
        /// <param name="StateObject">State object</param>
        /// <returns>Result of read action</returns>
        public SessionActionResult Read(string Key, SessionReadHandler Reader, object StateObject)
        {

            // Look for session using a reader lock.
            // If session is not found, return false;
            // If session is found:
            // Perform an atomic compare exchange on the variable 'InUse'
            // if session is in Use, try read again from the start.
            // if session is not in use, call delegate and return OK

            return Read(Key, Reader, StateObject, false);

        }

        /// <summary>
        /// Reads a stored session
        /// </summary>
        /// <param name="Key">Session Key</param>
        /// <param name="Reader">Method to call to complete read</param>
        /// <param name="StateObject">State object</param>
        /// <param name="isExporting">Indicates if the session is to be exported</param>
        /// <returns>Result of read action</returns>
        private SessionActionResult Read(string Key, SessionReadHandler Reader, object StateObject, bool isExporting)
        {
            if (Key == null) throw new ArgumentNullException("Key");

            bool tryAgain;
            bool sessionIslocked = false;

            Diags.ResetDeadLockCounter(); //Reset Dead lock counter

            do
            {
                tryAgain = false;
                AcquireReadLock();
                ISessionObject entry;
                try
                {
                    dict.TryGetValue(Key, out entry);
                }
                finally
                {
                    ReleaseReadLock();
                }

                if (entry == null)
                {
                    //Session not found
                    Diags.LogSessionNotFound(Key);
                    return SessionActionResult.NotFound;
                }
                else
                {
                    //Session Found
                    if (entry.CompareExchangeIsInUse(true, false) == false)
                    {
                        //The InUse flag has been set and now this thread has exclusive access to this session object
                        
                        try
                        {

                            //Set IsExporting flag for this entry if item is to be exported
                            if (isExporting)
                            {
                                entry.IsExporting = true;
                            }
  
                            //Call Reader Delegate
                            if(Reader != null) Reader(entry, StateObject);

                            if (isExporting)
                            {
                                Diags.LogSessionExporting(Key, entry); 
                            }
                            else
                            {
                                Diags.LogSessionRead(Key, entry);
                            }
                            sessionIslocked = entry.IsLocked;
                        }
                        finally
                        {
                            if (!isExporting) //Remove inUse property if not exporting
                            {
                                entry.CompareExchangeIsInUse(false, true);
                            }
                        }

                    }
                    else
                    {
                        //Nope, it's still there so check if it's been exported and try again

                        if (entry.IsExporting)
                        {
                            //This session is already been exported so leave
                            Diags.ResetDeadLockCounter();
                            return SessionActionResult.Exporting;
                        }
                        
                        Thread.Sleep(1); //pause for 1 ms
                        tryAgain = true;
                    }

                    Diags.DetectDeadLock(Key, DeadLockIterationCount); //Signal a deadlock after 2000 iterations

                }
            } while (tryAgain);

            Diags.ResetDeadLockCounter(); //Signal deadlock was freed

            if (sessionIslocked && !isExporting )
            {
                Diags.LogSessionIsLocked(Key);
                return SessionActionResult.Locked;
            }
            else
            {
                return SessionActionResult.OK;
            }
        }



        /// <summary>
        /// Begins an external session export
        /// </summary>
        /// <param name="Key">Session key</param>
        /// <param name="Reader">Method to call to kickstart export</param>
        /// <param name="StateObject">State object</param>
        /// <returns>Result of the operation</returns>
        public SessionActionResult BeginExport(string Key, SessionReadHandler Reader, object StateObject)
        {
            // Look for session using a reader lock.
            // If session is not found, return not found;
            // If session is found:
            // Perform an atomic compare exchange on the variable 'InUse'
            // if session is in Use, try read again from the start.
            // if session is not in use, call delegate and return OK response -- do not reset inuse property

            return Read(Key, Reader, StateObject, true);
        }
        

        /// <summary>
        /// Ends an external session export
        /// </summary>
        /// <param name="Key">Session key</param>
        /// <param name="RemoveSession">True to remove session from dictionary</param>
        public void EndExport(string Key, bool RemoveSession)
        {
            //This method resets the inuse property if the isExporting property is true

            if (Key == null) throw new ArgumentNullException("Key");

            AcquireReadLock();
            ISessionObject entry;
            try
            {
                dict.TryGetValue(Key, out entry);
            }
            finally
            {
                ReleaseReadLock();
            }

            if (entry == null)
            {
                //Session not found -- it's okay, don't freak out, session may have expired.
                return; 
            }
            else
            {
                //Session Found
                if (entry.IsInUse) 
                {
                    //The InUse flag, now check the isExporting flag
                    if (!entry.IsExporting)
                    {
                        Exception ex = new InvalidOperationException("EndExport must be called after a call to BeginExport");
                        Diags.LogApplicationError("EndExport must be called after a call to BeginExport -- Entry is InUse but IsExporting is false", ex);
                        throw ex;
                    }

                    try
                    {
                        //Delete session
                        if (RemoveSession)
                        {
                            AcquireWriteLock();
                            try
                            {
                                if (dict.Remove(Key))
                                {
                                    expiryList.Remove(Key);
                                    Diags.LogSessionDeleted(Key);
                                }

                            }
                            finally
                            {
                                ReleaseWriteLock();
                            }
                        }
                    }
                    finally
                    {
                        entry.IsExporting = false;
                        Diags.LogSessionExported(Key);
                        entry.CompareExchangeIsInUse(false, true);
                    }

                }
                else
                {
                    Exception ex = new InvalidOperationException("EndExport must be called after a call to BeginExport");
                    Diags.LogApplicationError("EndExport must be called after a call to BeginExport -- Entry is not in use", ex);
                    throw ex;
                }

            }

        }


        /// <summary>
        /// Gets the list of all keys in the session dictionary
        /// </summary>
        /// <remarks>
        /// This is useful to perform an operation on all keys in the session.
        /// However, this is a static list and the caller should be aware a key may no longer exist when the operation is performed
        /// </remarks>
        public List<string> Keys
        {
            get
            {
                AcquireReadLock();
                try
                {   
                    Dictionary<string, ISessionObject>.KeyCollection keys = dict.Keys;
                    List<string> keyList = new List<string>(keys);
                    return keyList;
                }
                finally
                {
                    ReleaseReadLock();
                }
            }
        }

        /// <summary>
        /// Removes all expired sessions from the dictionary
        /// </summary>
        public void Sweep()
        {
            List<string> keys = expiryList.GetOldKeys(DateTime.UtcNow);

            foreach (string key in keys)
            {
                //Get Timestamp
                DateTime timeStamp;                
                if (expiryList.TryGetTimeStamp(key, out timeStamp))
                {
                    //Make sure new timestamp is expired
                    if (DateTime.UtcNow > timeStamp)
                    {
                        //Expired
                        Expire(key, timeStamp);
                    }
                }
            }

        }

        #region Reader Writer Lock Acquisition/Release

        /// <summary>
        /// Acquires the session dictionary Read Lock
        /// </summary>
        private void AcquireReadLock()
        {
            #if NET20
                rwl.AcquireReaderLock(Timeout.Infinite);
            #else
                rwl.EnterReadLock();
            #endif
        }

        /// <summary>
        /// Releases the session dictionary Read Lock
        /// </summary>
        private void ReleaseReadLock()
        {
            #if NET20
                rwl.ReleaseReaderLock();
            #else
                rwl.ExitReadLock();
            #endif
        }

        /// <summary>
        /// Acquires the Session dictionary Write Lock
        /// </summary>
        private void AcquireWriteLock()
        {
            #if NET20
                rwl.AcquireWriterLock(Timeout.Infinite);
            #else
                rwl.EnterWriteLock();
            #endif
        }

        /// <summary>
        /// Releases the Session Dictionary Write Lock
        /// </summary>
        private void ReleaseWriteLock()
        {
            #if NET20
                rwl.ReleaseWriterLock();
            #else
                rwl.ExitWriteLock();
            #endif
        }

        #endregion

    }

    public enum SessionActionResult
    {
        OK, //Operation was successful
        Locked, //Resource is locked
        NotFound, //resource was not found
        AlreadyExists, //Resource already exists
        Exporting //Resource is being exported 
    }

    /// <summary>
    /// Represents a thread safe dictionary of key-value pairs sorted in place by their time stamps.
    /// </summary>
    /// <typeparam name="TKey">Type of item key</typeparam>
    /// <typeparam name="TValue">Type of item value</typeparam>
    class DateSortedDictionary<TKey, TValue>
    {
        List<TimeTaggedItem<TKey>> list = new List<TimeTaggedItem<TKey>>();
        Dictionary<TKey, TimeTaggedItem<TValue>> dict = new Dictionary<TKey, TimeTaggedItem<TValue>>();
        object sync = new object();

        /// <summary>
        /// Adds or updates an item in the dictionary
        /// </summary>
        /// <param name="TimeStamp">The timestamp to set for the item</param>
        /// <param name="Key">The Item Key</param>
        /// <param name="Value">The Item Value</param>
        public void Add(DateTime TimeStamp, TKey Key, TValue Value)
        {
            lock(sync)
            {
                TimeTaggedItem<TValue> entryValueItem;
                if (dict.TryGetValue(Key, out entryValueItem))
                {
                    //This item exists so update both dictionary and list

                    //First update list
                    //Look for existing item using the found entry
                    int index = BinaryLocate(Key, entryValueItem);

                    //Remove it
                    list.RemoveAt(index);

                    //Reinsert it in the right place in the ordered timestamp list 
                    //1. Look for next largest item in list for the new timestamp
                    TimeTaggedItem<TKey> listItem = new TimeTaggedItem<TKey>(TimeStamp, Key);
                    index = list.BinarySearch(listItem);
                    //2. Insert it there                    
                    list.Insert(index < 0 ? ~index : index, listItem);

                    //Secondly update dictionary with the value
                    dict[Key] = new TimeTaggedItem<TValue>(TimeStamp, Value);

                }
                else
                {
                    //This item does not exist so insert into both dictionary and list

                    //First insert into list
                    //Look for next largest item in list
                    TimeTaggedItem<TKey> listItem = new TimeTaggedItem<TKey>(TimeStamp,Key);
                    int index = list.BinarySearch(listItem);
                    //Insert it there                    
                    list.Insert(index < 0 ? ~index : index, listItem);
                    
                    //Secondly insert into dictionary
                    dict.Add(Key,new TimeTaggedItem<TValue>(TimeStamp,Value));
                }
            }
        }

        /// <summary>
        /// Scans for the location of an item within the internal list.
        /// </summary>
        /// <remarks>
        /// Performs a binary search within the list with forward and backward scanning.
        /// Will throw an exception if item is not found. USE ONLY to find an item by key when you KNOW it's there.
        /// </remarks>
        /// <param name="Key">Item Key</param>
        /// <param name="entryValueItem">The Item Value</param>
        /// <returns>The zer-based index of the items location</returns>
        private int BinaryLocate(TKey Key, TimeTaggedItem<TValue> entryValueItem)
        {
            int index = list.BinarySearch(new TimeTaggedItem<TKey>(entryValueItem.TimeStamp, Key));

            if (index < 0)
            {
                throw new InvalidOperationException("Use BinaryLocate only when the item exists in the List");
            }

            //This might not be the one corresponding to the right key
            int b = 0, f = 0;
            while (!list[index].Value.Equals(Key))
            {
                //So scan backwards and forwards silmultaneously
                b++;
                f++;

                if (index - b >= 0)
                {
                    if (list[index - b].Value.Equals(Key))
                    {
                        index = index - b;
                        break;
                    }
                }

                if (index + f <= list.Count - 1)
                {
                    if (list[index + f].Value.Equals(Key))
                    {
                        index = index + f;
                        break;
                    }
                }

                if ((index + f > list.Count - 1) && (index - b < 0))
                {
                    throw new InvalidOperationException("Use BinaryLocate only when the item exists in the List");
                }

            }
            return index;
        }

        /// <summary>
        /// Checks if the dictionary contains an item
        /// </summary>
        /// <param name="key">Item Key</param>
        /// <returns>True, if the item was found. Otherwise, false</returns>
        public bool ContainsKey(TKey key)
        {
            lock (sync)
            {
                return dict.ContainsKey(key);
            }
        }

        /// <summary>
        /// Removes an item from the dictionary
        /// </summary>
        /// <param name="Key">Item Key</param>
        /// <returns>true if item was removed. Otherwise, false.</returns>
        public bool Remove(TKey Key)
        {
            lock (sync)
            {
                TimeTaggedItem<TValue> entryValueItem;
                if (dict.TryGetValue(Key, out entryValueItem))
                {
                    //This item exists so remove both dictionary and list

                    //First remove from list
                    //Look for existing item using the found entry
                    int index = BinaryLocate(Key, entryValueItem);
                    //Remove item from the list
                    list.RemoveAt(index);

                    //Secondly remove from dictionary
                    dict.Remove(Key);

                    return true;

                }

                return false;

            }
        }

        /// <summary>
        /// Gets the value of an item
        /// </summary>
        /// <param name="key">Item Key</param>
        /// <param name="value">Item Value</param>
        /// <returns>True, if item value was obtained. Otherwise, false</returns>
        public bool TryGetValue(TKey key, out TValue value)
        {
            value = default(TValue);
            TimeTaggedItem<TValue> entry;
            lock(sync)
            {
                if (dict.TryGetValue(key, out entry))
                {
                    value = entry.Value;
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        /// <summary>
        /// Gets the time stamp value of an item
        /// </summary>
        /// <param name="Key">Key</param>
        /// <param name="TimeStamp">The Timestamp</param>
        /// <returns>True if timestamp was obtained. Otherwise, false.</returns>
        public bool TryGetTimeStamp(TKey Key, out DateTime TimeStamp)
        {
            TimeTaggedItem<TValue> entry;
            TimeStamp = DateTime.MinValue;
            lock (sync)
            {
                if (dict.TryGetValue(Key, out entry))
                {
                    //This item exists so return the value;
                    TimeStamp = entry.TimeStamp;
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        /// <summary>
        /// Gets the key of the item with the newest time stamp
        /// </summary>
        public TKey NewestKey
        {
            get
            {
                lock (sync)
                {
                    if (list.Count > 0)
                    {
                        return list[list.Count - 1].Value;
                    }
                    else
                    {
                        return default(TKey);
                    }
                }
            }
        }

        /// <summary>
        /// Gets the key of the item with the oldest time stamp
        /// </summary>
        public TKey OldestKey
        {
            get
            {
                lock (sync)
                {
                    if (list.Count > 0)
                    {
                        return list[0].Value;
                    }
                    else
                    {
                        return default(TKey);
                    }
                }
            }
        }

        /// <summary>
        /// Gets a list of all keys newer than a specified date
        /// </summary>
        /// <param name="DatedAfter">The specified date</param>
        /// <returns>A list of keys</returns>
        public List<TKey> GetNewKeys(DateTime DatedAfter)
        {
            List<TKey> keys = new List<TKey>();
            lock (sync)
            {
                if (list.Count > 0)
                {
                    for (int i = list.Count - 1; i >= 0; i--)
                    {
                        if (list[i].TimeStamp > DatedAfter)
                        {
                            keys.Add(list[i].Value);
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }
            return keys;
        }

        /// <summary>
        /// Gets a list of all keys older than a specified date
        /// </summary>
        /// <param name="DatedBefore">The specified date</param>
        /// <returns>List of keys</returns>
        public List<TKey> GetOldKeys(DateTime DatedBefore)
        {            
            List<TKey> keys = new List<TKey>();
            lock (sync)
            {
                if (list.Count > 0)
                {
                    for (int i = 0; i < list.Count; i++)
                    {
                        if (list[i].TimeStamp < DatedBefore)
                        {
                            keys.Add(list[i].Value);
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }
            
            return keys;

            
        }


        /// <summary>
        /// Gets the item with the newest time stamp
        /// </summary>
        public TimeTaggedItem<TValue> Newest
        {
            get
            {
                lock (sync)
                {
                    if (list.Count > 0)
                    {
                        return dict[list[list.Count - 1].Value];
                    }
                    else
                    {
                        return null;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the item with the oldest timestamp
        /// </summary>
        public TimeTaggedItem<TValue> Oldest
        {
            get
            {
                lock (sync)
                {
                    if (list.Count > 0)
                    {
                        return dict[list[0].Value];
                    }
                    else
                    {
                        return null;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the number of items in the dictionary
        /// </summary>
        public int Count
        {
            get
            {
                lock (sync)
                {
                    return list.Count;
                }
            }
        }

        /// <summary>
        /// Gets or sets the value for an item in the dictionary
        /// </summary>
        /// <param name="Key">Session key</param>
        /// <returns>The item value</returns>
        public TValue this[TKey Key]
        {
            get
            {
                TimeTaggedItem<TValue> entry;
                lock (sync)
                {
                    if (dict.TryGetValue(Key, out entry))
                    {
                        //This item exists so return the value;
                        return entry.Value;
                    }
                    else
                    {
                        throw new KeyNotFoundException("The key was not found in the dictionary");
                    }
                }
            }
            set
            {
                TimeTaggedItem<TValue> entry;
                bool found = false;
                lock (sync)
                {
                    if (dict.TryGetValue(Key, out entry))
                    {
                        //This item exists so update the value;
                        entry.Value = value;
                        found = true;
                    }
                }

                if (!found)
                {
                    this.Add(DateTime.MinValue, Key, value);
                }
                
            }
        }

    }

    /// <summary>
    /// Represents a time tagged item.
    /// </summary>
    /// <remarks>
    /// Note that this class can be used encapsulate a dictionary key or value
    /// </remarks>
    /// <typeparam name="T">The Type of the time tagged item's value</typeparam>
    class TimeTaggedItem<T> : IComparable<TimeTaggedItem<T>>
    {
        public DateTime TimeStamp;
        public T Value;

        /// <summary>
        /// Initializes a new instance of a TimeTaggedItem class
        /// </summary>
        /// <param name="TimeStamp">The timestamp to assign</param>
        /// <param name="Value">The value of the item</param>
        public TimeTaggedItem(DateTime TimeStamp, T Value)
        {
            this.TimeStamp = TimeStamp;
            this.Value = Value;
        }


        #region IComparable<DatedItem<TItem>> Members

        public int CompareTo(TimeTaggedItem<T> other)
        {
            return TimeStamp.CompareTo(other.TimeStamp);
        }

        #endregion
    }

    
}


By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Software Developer
United States United States
Sunny has been developing software for the Microsoft-based platforms since the MS-DOS days. He has coded in C, VB (4 to 6) and C#. He enjoys designing and developing server-side .NET distributed applications.

He currently works for a Fortune 500 company. When he's not coding, he likes reading, hanging out with friends and sight-seeing.

Comments and Discussions