An Approach to Session Usage

by Allan Sieker

The Overview

   “We do not pretend to have achieved perfection — but we do have a system — and it works.”  – Klaatu

When developing ASP.NET web applications a very common approach to maintaining state is to store variables into the Session.

Session is just one way to store data. Here are some other ones that I know of. Can you think of any others?

• Application (global) – uses server memory.
• Session (user) – can use user memory and/or database storage.
• Data cache (global, but can be user specific with proper handling) – uses server memory.
• Viewstate (web page) – rides along with the page.  No server memory.
• Database (global and user) – includes SQL, XML, local files, etc.
• Cookie (user) – stored on the client’s computer and accessed via the browser.
• Query String (user) – doesn’t use server memory – just a lot of developer patience.
• Form fields (web page) – popular in HTML and classic ASP development.

In the early days of Windows web development, storing to Session was limited to web applications with a small user base and a single web server because Session used memory on the web server and running on a web farm could send a request to a server that never saw you or the stuff you stuffed into the Session on another server. Fortunately, over the years Microsoft made improvements to support and encourage the use of Session. Examples include “Sticky Sessions” and State servers.

Session stores key-value pairs and virtually anything that can be serialized can be stored in the Session. However, just because you can doesn’t mean you should.

So, we all know that throwing things to Session is quick and easy. Just remember that a Session will expire. Then what do you do? If you were using Forms Authentication (very common), the user would need to log back in and start from scratch. Depending on what the user stands to lose, you may be better off storing the data to a database and saving Session for system level storage (i.e. User Id, Role, etc.) and control level storage (i.e. an editable paged data grid or grid view).

Ok. We know the pros and cons of using Session. Let’s move on to talking about Session usage.

The following diagram illustrates a way-too-typical web site storyboard where navigation can jump around a bit and each one of the four main trees is handled by a different developer. Let’s take a look at that diagram and talk about it for a bit…..

The Storyboard/Diagram (way-too-typical web site)

sessiondiscussion-thediagram2

 

The Disclaimer

Please keep in mind that this diagram is to facilitate a discussion on Session usage and may not reflect a well designed web solution.  So maybe you’ve seen this diagram like this before…
 

The Scenarios

We know that the developers will likely collaborate on the Session usage at the Main page level, but what will they do deep down inside their own tree?

Let’s walk through the scenarios below and maybe we can think of a few more…
The Cross-Pollinization Scenario

Suppose that the developer working on Sales (let’s call him SalesDev) needs to store a CustomerId to Session while the developer working on Accounting (let’s call him AcctDev) also needs to store a CustomerId to Session.   The storyboard shows that there is some cross-pollinization going on between Sales and Accounting.   How does SalesDev handle AcctDev overwriting the CustomerId value?
The Orphaned-Bloat Scenario

Suppose the Shipping page (under Orders) required the implementation of a very large transactional, editable paged data grid control where an entire dataset needed to be stored to Session and that the dataset was 10 meg in size.  Hey, it could happen.  Ok, let’s say 2 meg in size – but there are 10 concurrent users.   The point being that it all adds up.  

You accept that this scenario is a necessary evil that you intend to address by removing the dataset from Session when the user exits the page via the nice big EXIT button you have on the page.  Sounds like a plan – right?   Wrong!!!   You forgot about the browser’s back button or maybe the other links on the page (that the users wanted and were added later) that navigate to who knows where.   What do you do now?

Ok, so you add a method call to any event handler that navigates off the page.  I hope you documented that so the next developer knows to do the same thing.  You did add some comments, right?  I don’t know about you, but that sounds like a lot of work and baggage to me.

I could do more scenarios, but I’m close to the end of the page and time is getting short.  So let’s move on… 

 

The Approach

The recipe for this approach to Session usage takes planning, collaboration and a little bit of coding.  But before the reveal, let’s review a few short questions.

• What if you applied namespacing to session variable names?
• What if you could easily remove Session variables without needed to know their explicit names?
• What if you could do something like “RemoveAllSessionItemsLike(“Modules.*”)?

Well, what’s stopping you???? 

By now you should have already guessed the approach, so let’s see how it would apply to the storyboard.

Main page session variable names would start with System (yes, you could use Main).

Examples:

System.UserId
System.RoleId
System.UserName
Sub page session variable names would start with Sub.

Examples:

Sub.Inventory
Sub.Orders
Sub.Sales
Sub.Accounting

Sub.Inventory
Sub.Inventory.Warehouse
Sub.Inventory.Query
Sub.Inventory.Reports

Given a scenario where the user is busy working a specific customer in the Sales tree when a phone call comes in causing a quick diversion to look up a different customer over in the Accounting tree.   No worries if each tree has its own customer id:

Sub.Sales.CustomerId
Sub.Accounting.CustomerId
Building on the namespace convention, the use of wildcard patterns to blanket entire groups of Session variables becomes apparent.  For example, if we wanted to remove all Session variables under Sub.Inventory, the pattern Sub.Inventory.* would be used.
Clean Up Is Easy !!!

So far we have talked about removing session variables based on a wildcard specification.  Let’s implement a method to do this and call it RemoveFromSessionAllLike() where wildcard string is passed for the Session variable name patter we want to remove.

If all of the sub pages followed the Session namespacing convention, then all of the sessions created by the sub pages would all start with “Sub.”.

The logical place to remove all sub page Session variables is from the Main page.  This can be accomplished by calling RemoveFromSessionAllLike(“Sub.*”) from the Page_Load.
The Guidelines

• Create a namespace for all Session variables.

• The namespace should closely follow the modular layout of the web pages.

• All global Session variables must have the same namespace prefix.

• All non-global Session variables must use the same namespace prefix (that isn’t the same as the global prefix).

• All sub pages (aka modules) must use a unique namespace prefix that extends the non-global namespace prefix.

• Provide the RemoveFromSessionAllLike method as a shared static class. No need to instantiate it to use it.

• Include the RemoveFromSessionAllLike method call in the top level (i.e. Main) Page_Load

• Include the RemoveFromSessionAllLike method call in sub pages

• Prevent Session bloat as close to the source as possible
 

The Code

 

 

The main method for removal of Session variables:    

 

 public static void RemoveFromSessionAllLike(HttpSessionState session, String mask)

         {

         Regex regex = new Regex(mask, RegexOptions.IgnoreCase);

         NameObjectCollectionBase.KeysCollection keyList = session.Keys;

 

         ArrayList alKey = new ArrayList();

 

 

         foreach (String key in keyList)

            {

            if (regex.IsMatch(key))

               {

               alKey.Add(key);

               }

            }

 

         // Note: Needed to separate the session keys from the KeysCollection

            because an exception was thrown

         // when an attempt was made to directly alter the session from which

            the KeysCollection was derived from.

 

         foreach (String key in alKey)

            {

            // remove from Session

            session.Remove(key);

            }

 

         }

     


 

Support methods for retrieving Session variables (used by the test program):    

 

 

public static SortedList GetSessionManifest(HttpSessionState session)

         {

         SortedList list = new SortedList();

         NameObjectCollectionBase.KeysCollection keyList = session.Keys;

 

         foreach (String key in keyList)

            {

            list.Add(key, session[key].ToString());

            }

         return list;

         }

 

 

public static ArrayList GetSessionKeys(HttpSessionState session)

         {

         ArrayList alKey = new ArrayList();

         NameObjectCollectionBase.KeysCollection keyList = session.Keys;

 

         foreach (String key in keyList)

            {

               alKey.Add(key);

            }

 

         alKey.Sort();

 

         return alKey;

         }

 

 

 

public static ArrayList GetSessionKeysLike(HttpSessionState session, String mask)

         {

         ArrayList alKey = new ArrayList();

         NameObjectCollectionBase.KeysCollection keyList = session.Keys;

         Regex regex = new Regex(mask, RegexOptions.IgnoreCase);

 

         foreach (String key in keyList)

            {

            if (regex.IsMatch(key))

               {

               alKey.Add(key);

               }

            }

 

         alKey.Sort();

 

         return alKey;

         }


 

The test program:    

 

private void SessionTest()

         {

         // create session keys & values

         Session["Page.1"] = “1″;

         Session["Page.2"] = “2″;

         Session["Page.3"] = “3″;

         Session["Page.1.1"] = “1.1″;

         Session["Page.2.2"] = “2.2″;

         Session["Page.3.3"] = “3.3″;

 

         // display the session manifest (keys & values)

         SortedList items;

         items = GetSessionManifest(Page.Session);

         Response.Write(“Session manifest:<br>”);

         foreach (DictionaryEntry item in items)

            {

            Response.Write(String.Format(“{0} = {1}<br>”, item.Key, item.Value));

            }

         Response.Write(“<br><br>”);

 

         // display the session keys

         ArrayList list;

         list = GetSessionKeys(Page.Session);

         Response.Write(“Session keys:<br>”);

         foreach (String item in list)

            {

            Response.Write(item + “<br>”);

            }

         Response.Write(“<br><br>”);

 

 

 

 

         Response.Write(“Remove all session keys like Page.1.* <br><br>”);

         // remove the session keys that are like Page.*

         // regular expression pattern is used.

         RemoveFromSessionAllLike(Page.Session, “Page.1.*”);

 

 

 

 

         // show the session keys

         items = GetSessionManifest(Page.Session);

         Response.Write(“Session keys:<br>”);

         foreach (DictionaryEntry item in items)

            {

            Response.Write(String.Format(“{0} = {1}<br>”, item.Key, item.Value));

            }

         Response.Write(“<br><br>”);

         }

Leave a Reply