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

ASP.NET Session Helper (Scope, Categories)

Rate me:
Please Sign up or sign in to vote.
4.35/5 (12 votes)
9 May 2008CPOL7 min read 100.1K   1.1K   59   15
Use scopes and categories to avoid collision and confusion between your session values.

Introduction (more about the ASP.NET session)

If you are developing ASP.NET web applications, you surely had to store and retrieve some persistent values belonging to each user navigating through your ASP.NET Web Application’s pages, so you naturally have used Session.

More information on using the Session is available here: MSDN

Session is nothing but a Dictionary (a collection based on key/value pairs) whose key type is a string and the value type is an object (i.e., any serializable object inheriting from object).

You can access the Session from "everywhere" using the current HTTP context System.Web.HttpContext.Current.Session or from the page’s shortcut reference in System.Web.UI.Page.Session.

Concretely, when you want to store a value in the Session, you may write such a line of code (without taking care of any session manager):

C#
// Store value to session
int myValue = 10;
Session["MyKey"] = myValue;

Then, here is the code you may write to retrieve the previously stored value, mostly but not only from another page (or control):

C#
// Retrieve value from session (up cast needed) 
int myValue = (int)Session["MyKey"];

Note that if you write you own class and want it to be stored to Session, you’ll need to add the [Serializable] attribute.

Session is raw, here is SessionHelper

As you see, Session is somewhat raw, and you’ll be quickly annoyed when you want to use advanced features such as categorized values, values belonging to a page or a control, etc.

In these cases, you should (you must) use the Session, but you should have some collision problems, a day or another!

A solution to this problem (let me know your feedback about it) is to use "scoped and categorized" session values. This is not a feature of the ASP.NET framework, but a session helper that I built and used with success, so I’m pleased to share this tip with you.

What is a "scoped and categorized" session value? Nothing but a session value whose key is generated from several parameters: its scope (for example: "Global"), its optional category (for example: "Filter"), and its handle/key (for example: "ItemsPerPage").

So, instead of using raw key / value pairs, we’ll use formatted key / value pairs this way (note that each token is separated by dots):

Session["Scope.Category.Key"]

  • Available scopes are the following (you can invent and implement yours):
    • Global (the same scope as raw session)
    • Page (current page, excluding query string parameters)
    • PageAndQuery (current page, including query string parameters)
  • Available scope’s categories are just limited to your imagination, for example, "Visitor" or "Filter" ones.
  • The key is the name / handle to the final value; choose a short and clear one, for example: "Surname".

Thus, here is the way to store a global value using this scheme:

C#
/*
 *    In "OnePage.aspx"
 */
 
// Store value to session
Session["Global.Visitor.IPAddress"] = 
          HttpContext.Current.User.Identity.Name;
 
/*
 *    In "AnotherPage.aspx"
 */
 
// Retrieve value from session (up cast needed) 
string visitorIPAddress = 
       (string)Session["Global.Visitor.IPAddress"];

And, here is the code to restrict a stored value to a specific page (where "PageHash" is, for example, the MD5 or SHA-1 of the page’s URL):

C#
/*
 *    In "OnePage.aspx"
 */
 
// Store page's filter value to session
Session["PageHash.Filter.Surname"] = textBoxSurname.Text;
 
/*
 *    In "AnotherPage.aspx"
 */
 
// Retrieve value from session (downcast needed), WILL NOT WORKS!!
string onePageSurnameFilter = (string)Session["PageHash.Filter.Surname "];
 
/*
 *    In "OnePage.asp"
 */
 
// Retrieve value from session (downcast needed), WORKS!!
textBoxSurname.Text = (string)Session["PageHash.Filter.Surname"];

The principle is good, but the method is still raw and now difficult to use, so, it’s time to implement the session helper class.

Implementing the helper

To implement the "scoped and categorized" functionality, I chose to build up a helper class named SessionHelper (don’ blame me for the lack of originality ;)

You can use this class directly in your pages, but I recommend you make base classes inheriting from the ASP.NET framework ones (System.Web.UI.*) and wrap this helper class inside them. If you haven’t already done that in your Web application, it will save you some boring code (this is explained in the "Wrapping the helper" section).

Let's start the code with the class body:

C#
public class SessionHelper
{
    ...
}

As usual, let's start with enumerations. Here, I use one for the available scopes (global, to a page excluding query string, and to a page including query string):

C#
...

#region Enumerators

/// <summary>
/// Available session scopes
/// </summary>
public enum Scope
{
    Global,
    Page,
    PageAndQuery
    /* put any other scopes you may find useful here */
}

#endregion

...

There are no instance members or accessors, so we can directly start to write static methods. In order to make an exhaustive class, we must allow developers to store, retrieve, search, and clear values, but internally, we must start by writing some private utility functions. The first thing to do is to generate the formatted key. Here, it is done through some overloaded methods:

C#
#region Session's key format

/// <summary>
/// Format a key, using a scope a category and a key
/// </summary>
/// <param name="scope"></param>
/// <param name="category"></param>
/// <param name="key"></param>
/// <returns></returns>
private static string FormatKey(Scope scope, string category, string key)
{
    // clean up any points
    string scopeHash = GetScopeHash(scope);
    category = category.Replace(".", "");
    key = key.Replace(".", "");

    return string.Format("{0}.{1}.{2}", scopeHash, category, key);
}

/// <summary>
/// Format a key, using a category and a key (global scope)
/// </summary>
/// <param name="scope"></param>
/// <param name="key"></param>
/// <returns></returns>
private static string FormatKey(string category, string key)
{
    return FormatKey(Scope.Global, category);
}
 
/// <summary>
/// Format a key, using a scope and a key
/// </summary>
/// <param name="category"></param>
/// <param name="key"></param>
/// <returns></returns>
private static string FormatKey(Scope scope, string key)
{
    return FormatKey(scope, string.Empty);
}

/// <summary>
/// Format a key, using a key (global scope)
/// </summary>
/// <param name="scope"></param>
/// <param name="key"></param>
/// <returns></returns>
private static string FormatKey(string key)
{
    return FormatKey(string.Empty);
}

#endregion

You may have noticed a call to GetScopeHash; this method provides a hash corresponding to the given scope to avoid any collisions. Thus, "hard developers" will have to handle their custom scopes here, if needed (code below). Note that I call the GetHash method based on a MD5 hash (source: MSDN).

For its part, SessionKey provides a shortcut to HttpContext.Current.Session, according to the given scope, category, and handle.

StoreFormatedKey and ClearFormatedKey are low level methods to, respectively, store and retrieve a value to and from a formatted key.

Finally, ClearStartsWith is a helper method clearing all formatted keys, starting with the given string.

C#
#region Cryptography

/// <summary>
/// Creates a MD5 based hash
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
private static string GetHash(string input)
{
    // step 1, calculate MD5 hash from input
    System.Security.Cryptography.MD5 md5 = 
           System.Security.Cryptography.MD5.Create();
    byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
    byte[] hash = md5.ComputeHash(inputBytes);

    // step 2, convert byte array to hex string
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < hash.Length; i++)
    {
        sb.Append(hash[i].ToString("X2"));
    }
    return sb.ToString();
}

#endregion

/// <summary>
/// Get the hash of a scope
/// </summary>
/// <param name="scope"></param>
/// <returns></returns>
private static string GetScopeHash(Scope scope)
{
    // Get scope name
    string scopeName = Enum.GetName(scope.GetType(), scope);

    switch (scope)
    {
        case Scope.Page:
            scopeName = HttpContext.Current.Request.Url.AbsoluteUri;
            if (HttpContext.Current.Request.Url.Query != string.Empty)
            {
                scopeName = scopeName.Replace(
                   HttpContext.Current.Request.Url.Query, "");
            }
            break;
        case Scope.PageAndQuery:
            scopeName = HttpUtility.UrlDecode(
               HttpContext.Current.Request.Url.AbsoluteUri);
            break;
    }

    return GetHash(scopeName);
}

/// <summary>
/// Shortcut to formated session value
/// </summary>
/// <param name="scope"></param>
/// <param name="category"></param>
/// <param name="key"></param>
/// <returns></returns>
private static object SessionKey(Scope scope, string category, string key)
{
    return HttpContext.Current.Session[FormatKey(scope, category, key)];
}                               

/// <summary>
/// Rawly store a value in a formated key
/// </summary>
/// <param name="formatedKey"></param>
/// <param name="value"></param>
private static void StoreFormatedKey(string formatedKey, object value)
{
    HttpContext.Current.Session[formatedKey] = value;
}
 
/// <summary>
/// Rawly clear a formated key value
/// </summary>
/// <param name="formatedKey"></param>
private static void ClearFormatedKey(string formatedKey)
{
    HttpContext.Current.Session.Remove(formatedKey);
}

/// <summary>
/// Clears all formated keys starting with given value
/// </summary>
/// <param name="startOfFormatedKey"></param>
private static int ClearStartsWith(string startOfFormatedKey)
{
    List<string> formatedKeysToClear = new List<string>();

    // Gather formated keys to clear
    // (to prevent collection modification during parsing)
    foreach (string key in HttpContext.Current.Session)
    {
        if (key.StartsWith(startOfFormatedKey))
        {
            // Add key
            formatedKeysToClear.Add(key);
        }
    }

    foreach (string formatedKey in formatedKeysToClear)
    {
        ClearFormatedKey(formatedKey);
    }

    return formatedKeysToClear.Count;
}

#endregion

It’s now time to check if a key exists (basically done by checking if the session value is null), with the help of these overloaded methods:

C#
#region Key existence

/// <summary>
/// Indicates if the key associated to given scope and category exists
/// </summary>
/// <param name="scope"></param>
/// <param name="category"></param>
/// <param name="key"></param>
public static bool Exists(Scope scope, string category, string key)
{
    return SessionKey(scope, category, key) != null;
}

/// <summary>
/// Indicates if the key associated to given category exists (global scope)
/// </summary>
/// <param name="category"></param>
/// <param name="key"></param>
public static bool Exists(string category, string key)
{
    return Exists(Scope.Global, category, key);
}

/// <summary>
/// Indicates if the key associated to given scope exists
/// </summary>
/// <param name="scope"></param>
/// <param name="key"></param>
public static bool Exists(Scope scope, string key)
{
    return Exists(scope, string.Empty);
}

/// <summary>
/// Indicates if the key exists (global scope)
/// </summary>
/// <param name="key"></param>
public static bool Exists(string key)
{
    return Exists(string.Empty, key);
}

#endregion

Then, we provide some overloaded methods whose aim is to store values, according to the scope, category, and key (note that we use the StoreFormattedKey here):

C#
#region Values storing

/// <summary>
/// Stores a value to session, using a scope a category and a key
/// </summary>
/// <param name="scope"></param>
/// <param name="category"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void Store(Scope scope, string category, string key, object value)
{
    StoreFormattedKey(FormatKey(scope, category, key), value);
}

/// <summary>
/// Stores a value to session, using a category and a key (global scope)
/// </summary>
/// <param name="category"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void Store(string category, string key, object value)
{
    Store(Scope.Global, category, key, value);
}

/// <summary>
/// Stores a value to session, using a scope and a key
/// </summary>
/// <param name="scope"></param>
/// <param name="key"></param>
public static void Store(Scope scope, string key, object value)
{
    Store(scope, string.Empty, key, value);
}

/// <summary>
/// Stores a value to session, using a key (global scope)
/// </summary>
/// <param name="key"></param>
public static void Store(string key, object value)
{
    Store(string.Empty, key, value);
}#endregion

After storing some values, the logical step is to retrieve them. To avoid some boring code to developers using our class, we’ll provide basic retrieving (Retrieve) and the ability to get a default value if the waited value doesn’t exist (RetrieveWithDefault):

C#
#region Values retrieving (null if not found)

/// <summary>
/// Stores a value to session, using a scope a category and a key
/// </summary>
/// <param name="scope"></param>
/// <param name="category"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static object Retrieve(Scope scope, string category, string key)
{
    return SessionKey(scope, category, key);
}

/// <summary>
/// Stores a value to session, using a category and a key (global scope)
/// </summary>
/// <param name="category"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static object Retrieve(string category, string key)
{
    return Retrieve(Scope.Global, category, key);
}

/// <summary>
/// Stores a value to session, using a scope and a key
/// </summary>
/// <param name="scope"></param>
/// <param name="key"></param>
public static object Retrieve(Scope scope, string key)
{
    return Retrieve(scope, string.Empty, key);
}

/// <summary>
/// Stores a value to session, using a key (global scope)
/// </summary>
/// <param name="key"></param>
public static object Retrieve(string key)
{
    return Retrieve(string.Empty, key);
}

#endregion
 
 
#region Values retrieving (with default value)

/// <summary>
/// Stores a value to session, using a scope a category and a key
/// </summary>
/// <param name="scope"></param>
/// <param name="category"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static object RetrieveWithDefault(Scope scope, 
       string category, string key, object defaultValue)
{
    object value = SessionKey(scope, category, key);

    return value == null ? defaultValue : value;
}

/// <summary>
/// Stores a value to session, using a category and a key (global scope)
/// </summary>
/// <param name="category"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static object RetrieveWithDefault(string category, 
                     string key, object defaultValue)
{
    return RetrieveWithDefault(Scope.Global, category, key, defaultValue);
}

/// <summary>
/// Stores a value to session, using a scope and a key
/// </summary>
/// <param name="scope"></param>
/// <param name="key"></param>
public static object RetrieveWithDefault(Scope scope, 
              string key, object defaultValue)
{
    return RetrieveWithDefault(scope, string.Empty, key, defaultValue);
}

/// <summary>
/// Stores a value to session, using a key (global scope)
/// </summary>
/// <param name="key"></param>
public static object RetrieveWithDefault(string key, object defaultValue)
{
    return RetrieveWithDefault(string.Empty, key, defaultValue);
}

#endregion

The last step is the capability to clear stored values. We can clear all values (Clear), the ones belonging to a scope (ClearScope), a scope’s category (ClearCategory), or simply to a key (Clear overloaded methods):

C#
#region Values clearing

/// <summary>
/// Clears all session values
/// </summary>
public static void Clear()
{
    HttpContext.Current.Session.Clear();
}

/// <summary>
/// Clears all  session values of given scope
/// </summary>
/// <param name="scope"></param>
/// <returns>Number of affected values</returns>
public static int ClearScope(Scope scope)
{
    return ClearStartsWith(string.Format("{0}.", GetScopeHash(scope)));
}

/// <summary>
/// Clears all session values of given scope's category
/// </summary>
/// <param name="scope"></param>
/// <param name="category"></param>
public static int ClearCategory(Scope scope, string category)
{
    return ClearStartsWith(string.Format("{0}.{1}.", 
                           GetScopeHash(scope), category));
}

/// <summary>
/// Clears all session values of given category (global scope)
/// </summary>
/// <param name="category"></param>
public static int ClearCategory(string category)
{
    return ClearCategory(Scope.Global, category);
}

/// <summary>
/// Clears a session value, using a scope a category and a key
/// </summary>
/// <param name="scope"></param>
/// <param name="category"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void Clear(Scope scope, string category, string key)
{
    Store(scope, category, key, null);
}
 
/// <summary>
/// Clears a session value, using a category and a key (global scope)
/// </summary>
/// <param name="category"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void Clear(string category, string key)
{
    Clear(Scope.Global, category, key);
}

/// <summary>
/// Clears a session value, using a scope and a key
/// </summary>
/// <param name="scope"></param>
/// <param name="key"></param>
public static void Clear(Scope scope, string key)
{
    Clear(scope, string.Empty, key);
}

/// <summary>
/// Clears a session value, using a key (global scope)
/// </summary>
/// <param name="key"></param>
public static void Clear(string key)
{
    Clear(string.Empty, key);
}

#endregion

That’s all folks!

Using the SessionHelper in your project

This helper class is easy to incorporate and use in your project. You can:

  1. move the class code in to your solution
  2. use the Initia.Web project included in the sample project
  3. compile and reference the Initia.Web project’s assembly.

Once you’ve done this, you may use the Initia.Web namespace like this:

C#
using Initia.Web;

Then, you can start to use the helper class. Let’s say you have a couple of pages, and you want to count the total hits, each page’s hits, and the hits belonging to the click on a button.

First, create a page and go to the Page_Load event handler. We don’t have to bother about the IsPostBack state since we want to increment hits at each page load.

C#
protected void Page_Load(object sender, EventArgs e)
{
    ...
}

Here is the code to retrieve the (implicit) global scope "Hits" session value, with the default value set to 0. Then, we store the incremented value:

C#
...

// Incremented global session value
// (note that it doesn't scope with other "Hits" keys)
int totalHits = (int)SessionHelper.RetrieveWithDefault("Hits", 0);
SessionHelper.Store("Hits", totalHits + 1);

...

A very close code, using the SessionHelper.Scope.Page scope, can be written to be used in the scope of the current page (without bothering about the query string). Note that we’re using the same key, but there is no collision since we’re using another scope:

C#
...

// Incremented current page session value
// (note that it doesn't scope with other "Hits" keys)
int currentPageHits = (int)SessionHelper.RetrieveWithDefault(
                       SessionHelper.Scope.Page, "Hits", 0);
SessionHelper.Store(SessionHelper.Scope.Page, "Hits", ++currentPageHits);

...

If you want to restrict the scope of some session values to the scope of the current page, including the query string, you may write this piece of code using the SessionHelper.Scope.PageAndQuery scope:

C#
...

// Incremented current page session value
// (note that it doesn't scope with other "Hits" keys)
int currentPageQueryHits = (int)SessionHelper.RetrieveWithDefault(
                            SessionHelper.Scope.PageAndQuery, "Hits", 0);
SessionHelper.Store(SessionHelper.Scope.PageAndQuery, 
                    "Hits", ++currentPageQueryHits);
...

The last example is about categorized session values. This time, we’re going to increment a hits counter each time the user clicks on a button, and we’ll clear the category when the user clicks another button. Just add buttons in your page like this:

ASP.NET
<asp:Button ID="btnAddUserHit" Text="Add hit to user category" runat="server" />
<asp:Button ID="btnClearUserCategory" Text="Clear user category" runat="server" />

Then, add an event handler in the code-behind (you can do it in the ASPX page, if you want):

C#
protected override void OnInit(EventArgs e)
{
    base.OnInit(e);

    // Events handlers

    btnAddUserHit.Click += new EventHandler(btnAddUserHit_Click);
    btnClearUserCategory.Click += new EventHandler(btnClearUserCategory _Click);
}

And finally, implement the event handlers like the following (UpdateUI is explained below):

C#
#region Events handlers

private void btnAddUserHit_Click(object sender, EventArgs e)
{
    // Retrieves, increments then stores
    // categorized session value (default value is 0)
    int userHits = (int)SessionHelper.RetrieveWithDefault(
                        "User", "Hits", 0);
    SessionHelper.Store("User", "Hits", userHits + 1);

    // Update user interface
    UpdateUI();
}

private void btnClearGlobalCategory_Click(object sender, EventArgs e)
{
    SessionHelper.ClearCategory(SessionHelper.Scope.Global, "User");

    // Update user interface
    UpdateUI();
}

#endregion

As "bonus", here is a simple method updating the user interface according to the session values (just add a textbox with a tbUserHits ID in your ASPX page):

C#
#region User interface

/// <summary>
/// Update user interface
/// </summary>
public void UpdateUI()
{
    // Show categorized session value (default value is 0)
    tbUserHits.Text = SessionHelper.RetrieveWithDefault("User", 
                                         "Hits", 0).ToString();
}

#endregion

Don’t forget to download the sample project; it covers most of the class capabilities, and will speed up the class integration in your project.

Points of interest / Conclusion

I hope this class will help you in your project and will save you some precious time.

If you don’t like my method and have a better one, please let us know in the comments are. If it helps you, please leave a comment and a mark to encourage me.

Thanks to

  • Rafael Rosa for fixing the encoding bug in the private static string GetScopeHash(Scope scope) method.

History

  • 2008 / 04 / 18 : First version.
  • 2008 / 04 / 19 : Added the "Use the SessionHelper in your project" section.
  • 2008 / 05 / 10 : Fixed the encoding bug in the private static string GetScopeHash(Scope scope) method (thanks to Rafael Rosa for his contribution). Both the class and the sample project have been updated.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer Stambia
France France
I'm a 39 year old team deputy CTO, living and working near Lyon (France)

I started to write softwares in the end of 90's, when I was a teenager.

I acquired several but complementary skills, mainly (but not only) on Microsoft's technologies and platforms : assembly (x86, 68k), C, C++, .NET/C#, JavaScript/HTML/CSS, PHP, DBMS (MySQL, Oracle, SQL Server, etc.), etc.

During 2007-2012, I was particulary active on .NET, ASP.NET, C#, SQL Server and ORM (NHibernate) with a growing time spent on architecture, technical management, code review, etc.

Since 2013, I'm a full-time development team leader/manager at Everial, I stopped development at work but still love to develop some personnal programs during my spare time, mainly for domotic purposes. I use to play with Arduino, ESP8266/NodeCmu, Raspberry PI... running them with some C++ and .NET Core.

My hobbies are futsal, badminton, motorcycle, Formula One, domotic, gardening... and software developement.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 1248326825-Apr-16 1:32
Member 1248326825-Apr-16 1:32 
QuestionWhat is the advantage of using this sessionhelper? Pin
Srinath Gopinath12-May-08 19:48
Srinath Gopinath12-May-08 19:48 
AnswerRe: What is the advantage of using this sessionhelper? Pin
Florian DREVET13-May-08 2:04
Florian DREVET13-May-08 2:04 
GeneralRe: What is the advantage of using this sessionhelper? Pin
Rafael Rosa Fu14-May-08 12:28
Rafael Rosa Fu14-May-08 12:28 
GeneralRe: What is the advantage of using this sessionhelper? Pin
Florian DREVET15-May-08 22:05
Florian DREVET15-May-08 22:05 
GeneralSessionHelper - Encoding Problems Pin
Rafael Rosa Fu8-May-08 6:40
Rafael Rosa Fu8-May-08 6:40 
AnswerRe: SessionHelper - Encoding Problems Pin
Florian DREVET9-May-08 23:03
Florian DREVET9-May-08 23:03 
GeneralSession helper Pin
Rafael Rosa Fu6-May-08 11:04
Rafael Rosa Fu6-May-08 11:04 
GeneralRe: Session helper Pin
Florian DREVET9-May-08 23:00
Florian DREVET9-May-08 23:00 
GeneralLooks useful Pin
TonyDyer25-Apr-08 5:23
TonyDyer25-Apr-08 5:23 
GeneralRe: Looks useful Pin
Florian DREVET25-Apr-08 23:26
Florian DREVET25-Apr-08 23:26 
GeneralGreat work - removing in scope Pin
eraseunavez21-Apr-08 20:36
eraseunavez21-Apr-08 20:36 
GeneralRe: Great work - removing in scope Pin
Florian DREVET23-Apr-08 19:53
Florian DREVET23-Apr-08 19:53 
GeneralClearing session values. Pin
Edmundas Kevisas18-Apr-08 0:52
Edmundas Kevisas18-Apr-08 0:52 
GeneralRe: Clearing session values. [modified] Pin
Florian DREVET18-Apr-08 3:16
Florian DREVET18-Apr-08 3:16 

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

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