Click here to Skip to main content
Click here to Skip to main content
Go to top

How to Deal with those pesky Session Objects between page redirects!

, 13 Jan 2006
Rate this:
Please Sign up or sign in to vote.
This article gives a solution for cleaning up session variables that leap from one page to another in unexpected ways.

Introduction

UPDATED!!! (This article is a re-write of a less descriptive earlier version) One of the most prevalent problems in dealing with web applications is the management of session variables. You put one out there for a page and the next thing you know is it gets lost in the ever increasing maze of web pages and variables. Some time later you start seeing some unexpected (and quite maddening) behavior that you can't quite get a handle on. Some examples are seeing data that you thought was gone from a query or had been refreshed....you know it if you have struggled with such a problem. Or another very common problem behind cleaning up session variables between page redirects and the main problem this article is a solution for is when your users do not use the page buttons you have that contain the code to remove these session variables, but simply hit a menu item or some other unexpected action that forgoes your nice little session cleanup script. That's when you get calls saying that the users were on such and such page, changed briefly using the menu bar at the top of a page to another and then came back into your page with the same data as before, except now they wanted customer XXXXX instead of the old values of customer YYYYYY.

So, how do you make light work of cleaning up session variables that leap from page to page in unexpected ways? Check out my solution below.

Using the code

The first thing I want to state is that this is not an end-all be-all for session state management. It is simply a neat little way to allow you to have your code automatically help keep rouge session objects from messing up your day. We are going to do something very simple and something a little complex. The simple part is we are going to remove the unwanted session variables from page calls as each page is called, but leave the ones we want alone. The complex part is we are going to do this by looking at the page URL, and trying to match this URL via an XML file with a session key name. The session key and the URL are matched together by you the developer in a file we will call for this article SessionMatrix.xml. Let's take a look at it:

<?xml version="1.0" encoding="utf-8" ?>
<sessionKeys>
  <sessionKey name="CampaignID">
      <page url="~/Secure/ManageCampaign.aspx" />
      <page url="~/Secure/EditCampaign.aspx" />
      <page url="~/Secure/CopyCampaign.aspx" />
      <page url="~/Secure/CreateCampaign.aspx" />
      <page url="~/Secure/ManageSegments.aspx" />
      <page url="~/Secure/ManageRules.aspx" />
      <page url="~/Secure/ManageCells.aspx" />
      <page url="~/Secure/ModifySQL.aspx" />
      <page url="~/Secure/ListManagement.aspx" />
  </sessionKey>
  <sessionKey name="SegmentID">    
      <page url="~/Secure/ManageSegments.aspx" />
      <page url="~/Secure/ManageRules.aspx" />
      <page url="~/Secure/ManageCells.aspx" />
      <page url="~/Secure/ModifySQL.aspx" />
      <page url="~/Secure/ListManagement.aspx" />
  </sessionKey>
  <sessionKey name="RuleID">
      <page url="~/Secure/ManageRules.aspx" />    
      <page url="~/Secure/ModifySQL.aspx" /> 
  </sessionKey>
  <sessionKey name="CellID">
      <page url="~/Secure/ManageCells.aspx" />    
  </sessionKey>
</sessionKeys>

Notice we have XML nodes called sessionkeys. The name attribute here is the actual string key name of the session ID key which you wish to map to a specific page. In other words, this session object can only be a part of the session while on these pages. We notice that under each sessionKey node we have a node called page and an associated url. This is the application level URL we want to match to the session key.

Only registered session objects are handled, unregistered ones are ignored.

OK, now let's talk about how the code works. To use session management in your application, you simply inherit your pages from GlobalPage, which has the check in the OnInit event:

/// <summary>
/// Inherit all pages from GlobalPage 
/// that you want session management for.
/// </summary>
public class GlobalPage : System.Web.UI.Page
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="e"></param>
    protected override void OnInit(System.EventArgs e)
    {
        //this is the best place to put the check, 
        //since it fires on every page load
        //and occurs before any other page processing
        SessionUrlFactory.CheckSession(this);        
    }

This is just to get the factory to check out your session variables on every call to a page. You could build an ISAPI filter as well and put SessionUrlFactory.CheckSession(this) in there.

Now that we have the session key objects linked to the URLs in your XML file we can see how this is reflected during runtime by the code.

CheckSession is the method that gets called to check and invalidate all the session objects registered in the factory class. Only session objects registered are cleared, and they are only cleared if the URL in Request.RawUrl does not match one of the URLs in the SessionUrlData object for that session key. I also added some code to strip off query strings from the end so that we can use RegEx to do a good match. We call a method to load the XML data if this is not already loaded and then parse through this data with RegEx to check out the registered session variables. We also use the FileSystemWatcher to make changes if the XML file is modified:

/// <summary>
/// checks the current url, and decides whether the seesion keys
/// match the url data in the SessionUrlData for each key.
/// If no page in SessionUrlData matches the current url
/// then the session value is set to null via the indexer
/// (I used remove but caused an collection fault because 
/// it changed the size of the collection inside the loop
/// </summary>
/// <param name="page"></param>
public static void CheckSession(GlobalPage page)
{
     if (!_isLoaded)
    {
        _filePath = 
           page.Server.MapPath("~/App_Data/SessionMatrix.xml");
        LoadData();
        SetupWatcher(
           new System.IO.FileSystemEventHandler(Config_Changed));
    }
    string pathOnly = page.Request.RawUrl;
    if(pathOnly.IndexOf('?') >= 0)
        pathOnly = pathOnly.Remove(0,pathOnly.IndexOf('?'));

    Regex regex = new Regex(pathOnly, 
                      RegexOptions.IgnorePatternWhitespace);
    ArrayList sessionKeysToRemove = new ArrayList();
    foreach(string sessionKey in page.Session.Keys)
    {
        SessionUrlData sessionUrlData = 
          (SessionUrlData)_registeredSessionUrlData[sessionKey];
        if(sessionUrlData == null) continue;
        bool isValidSessionKey = false;
        foreach(string url in sessionUrlData)
            if(regex.IsMatch(url))
            {
                isValidSessionKey = true;
                break;
            }
                
        if(!isValidSessionKey) 
            sessionKeysToRemove.Add(sessionKey);
            
    }
    //do the session modifications here 
    //because when done inside the loop
    //the collection indexer throws 
    //and System OverFlow exception
    foreach(string key in sessionKeysToRemove)
        try{page.Session.Remove(key);}
        catch(Exception){}
}

The first thing we notice in the method is that we are checking a static boolean variable to see if we have already loaded the data. I did this in this method instead of the constructor at first to test the code using the Server.MapPath() method, but now it probably is a un-needed step for each method call and can go back in the constructor of the factory and use some other method to get the file like using the assembly to get the BaseName of the assembly:

static SessionUrlFactory()
{
    _filePath = AppDomain.CurrentDomain.BaseDirectory + 
                              "App_Data/SessionMatrix.xml"); 
    LoadData(); 
    SetupWatcher(
      new System.IO.FileSystemEventHandler(Config_Changed)); 
}

Next, we are actually getting the path from the request's RawUrl attribute, which gives us the entire URL path, which we will strip off its query string and use with the RegEx object to match against the URLs for the session keys registered. We are going to get all the session keys for the page and check to see if they are registered session keys. If they are, we will get the SessionUrlData object out of the static registry collection for the factory:

foreach(string sessionKey in page.Session.Keys)
{
    SessionUrlData sessionUrlData = 
      (SessionUrlData)_registeredSessionUrlData[sessionKey];
    if(sessionUrlData == null) continue;
    bool isValidSessionKey = false;

If the SessionUrlData object is not null for this session key, we know we have a registered session key. The next step is to loop through this collection object and try to see if any of the URLs match.

Here is the tricky part. If the RegEx object matches with the sessionkey url we will leave it in the session, if not we will remove it after we check all the possible URLs for each session key. This is how we perform our magic of session object cleanup!

if(sessionUrlData == null) continue;
bool isValidSessionKey = false;
foreach(string url in sessionUrlData)
    if(regex.IsMatch(url))
    {
        isValidSessionKey = true;
        break;
    }
      
if(!isValidSessionKey)
    sessionKeysToRemove.Add(sessionKey);

If you find a problem in how the URL looks after it is loaded into the SessionUrlData object then modifying the LoadData() method, which we will discuss later, can help you fix any problems that may come up.

The bulk of the functionality lies in the CheckSession method. But let's also look at how a few other objects and methods are important in helping to interact with this method to make it work.

The LoadData() method is where we translate the XML file we created above into viable URLs for each session key we have registered. One thing to notice below is how we are using the ConfigurationSettings.AppSettings["ApplicationName"] method to retrieve the application name from the appSettings section of the CONFIG file. I do this here because this was tested on a server which uses virtual directories instead of a root or enterprise web application. If it doesn't work then you will need to change this appropriately to get the needed URL match while accessing Page.Request.RawUrl:

private static void LoadData()
{
    XmlDocument doc = new XmlDocument();
    doc.Load(_filePath);
    XmlNodeList list = doc.GetElementsByTagName("sessionKey");
    for (int i = 0; i < list.Count; i++)
    {
        SessionUrlData sessionUrlData = null;
        XmlAttributeCollection attributes = list[i].Attributes;
        foreach(XmlAttribute attribute in attributes)
        {
            if (attribute.Name == "name")
              sessionUrlData = new SessionUrlData(attribute.Value);
        }
        if(sessionUrlData == null) continue;
        XmlNodeList pages = list[i].ChildNodes;
        foreach(XmlNode page in pages)
        {
            XmlAttributeCollection innerAttributes = page.Attributes;
            foreach(XmlAttribute attribute in innerAttributes)
            {
              if (attribute.Name == "url")
                sessionUrlData.AddPage(
                   attribute.Value.Replace("~/", "/" + 
                   ConfigurationSettings.AppSettings["ApplicationName"] + 
                   "/"));
            }
        }
        RegisterSessionUrlData(sessionUrlData);
    }
    _isLoaded = true;
}

Let's look briefly at the SessionUrlData class. This class is just a holder class for the URLs. It is a way to group them for storage in the static factory registry collection. It is really just an encased collection class set up with an enumerator and specialized collection methods. It uses an ArrayList for its underlying collection object. This is a clumsy implementation of the Iterator pattern I realize, but it works so who cares?

public class SessionUrlData
{
    private string _sessionKey;
    private ArrayList _validPages = new ArrayList();
  
    public SessionUrlData(string sessionKey)
    {
        _sessionKey = sessionKey;          
    }
  
    public string SessionKey
    {
        get{return _sessionKey;}
    }
  
    public void AddPage(string pageUrl)
    {
        _validPages.Add(pageUrl);
    }
  
    internal int Add(string url)
    {
        return _validPages.Add(url);
    }
  
    internal void Clear()
    {
        _validPages.Clear ();
    }

    public int Capacity
    {
        get
        {
            return _validPages.Capacity;
        }
        set
        {
            _validPages.Capacity = value;
        }
    }
  
    public bool Contains(string url)
    {
        return _validPages.Contains(url);
    }
 
    public int Count
    {
        get
        {
            return _validPages.Count;
        }
    }

    public IEnumerator GetEnumerator()
    {
        return _validPages.GetEnumerator();
    }

    public bool Equals(SessionUrlData obj)
    {
        return _validPages.Equals (obj);
    }

    public override int GetHashCode()
    {
        return _validPages.GetHashCode();
    }
 
    public int IndexOf(string url)
    {
        return _validPages.IndexOf(url);
    }
 
    public int IndexOf(string url, int startIndex)
    {
        return _validPages.IndexOf(url, startIndex);
    }

    public int IndexOf(string url, int startIndex, int count)
    {
        return _validPages.IndexOf (url, startIndex, count);
    }
 
    public string this[int index]
    {
        get
        {
            return (string) _validPages[index];
        }
        set
        {
            _validPages[index] = (string) value;
        }
    }
}

To implement this in your web application simply point the path that is used in the CheckSession method to the SessionMatrix.xml file location, decide which session variables to which files you wish to manage and make those changes to the XML file, and you are off! No more crazy session problems for you!

Points of interest

This was tested on a server which uses virtual directories so I am wondering if it works fine across root or enterprise web applications.

History

This is the second submission to CodeProject on this subject and is the second revision.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Christopher G. Lasater
Web Developer
United States United States
Christopher G. Lasater
 
I am also a published author, please check out my book:
ISBN: 1-59822-031-4
Title: Design Patterns
Author:Christopher G. Lasater
More from my web site
Amazon.com


Comments and Discussions

 
Generalusing the cache Pinmemberderhackler18-Jan-06 7:10 
GeneralRe: using the cache Pinmemberchris lasater18-Jan-06 8:47 
GeneralRe: using the cache Pinmemberderhackler18-Jan-06 10:35 
GeneralRe: using the cache Pinmemberchris lasater18-Jan-06 11:08 
GeneralRe: using the cache Pinmemberderhackler19-Jan-06 7:25 
GeneralRe: using the cache Pinmemberchris lasater19-Jan-06 7:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140916.1 | Last Updated 13 Jan 2006
Article Copyright 2006 by Christopher G. Lasater
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid