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:
="1.0"="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:
public class GlobalPage : System.Web.UI.Page
{
protected override void OnInit(System.EventArgs e)
{
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:
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);
}
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.