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):
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):
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:
Session["Global.Visitor.IPAddress"] =
HttpContext.Current.User.Identity.Name;
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):
Session["PageHash.Filter.Surname"] = textBoxSurname.Text;
string onePageSurnameFilter = (string)Session["PageHash.Filter.Surname "];
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:
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):
...
#region Enumerators
public enum Scope
{
Global,
Page,
PageAndQuery
}
#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:
#region Session's key format
private static string FormatKey(Scope scope, string category, string key)
{
string scopeHash = GetScopeHash(scope);
category = category.Replace(".", "");
key = key.Replace(".", "");
return string.Format("{0}.{1}.{2}", scopeHash, category, key);
}
private static string FormatKey(string category, string key)
{
return FormatKey(Scope.Global, category);
}
private static string FormatKey(Scope scope, string key)
{
return FormatKey(scope, string.Empty);
}
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.
#region Cryptography
private static string GetHash(string input)
{
System.Security.Cryptography.MD5 md5 =
System.Security.Cryptography.MD5.Create();
byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
byte[] hash = md5.ComputeHash(inputBytes);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
{
sb.Append(hash[i].ToString("X2"));
}
return sb.ToString();
}
#endregion
private static string GetScopeHash(Scope scope)
{
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);
}
private static object SessionKey(Scope scope, string category, string key)
{
return HttpContext.Current.Session[FormatKey(scope, category, key)];
}
private static void StoreFormatedKey(string formatedKey, object value)
{
HttpContext.Current.Session[formatedKey] = value;
}
private static void ClearFormatedKey(string formatedKey)
{
HttpContext.Current.Session.Remove(formatedKey);
}
private static int ClearStartsWith(string startOfFormatedKey)
{
List<string> formatedKeysToClear = new List<string>();
foreach (string key in HttpContext.Current.Session)
{
if (key.StartsWith(startOfFormatedKey))
{
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:
#region Key existence
public static bool Exists(Scope scope, string category, string key)
{
return SessionKey(scope, category, key) != null;
}
public static bool Exists(string category, string key)
{
return Exists(Scope.Global, category, key);
}
public static bool Exists(Scope scope, string key)
{
return Exists(scope, string.Empty);
}
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):
#region Values storing
public static void Store(Scope scope, string category, string key, object value)
{
StoreFormattedKey(FormatKey(scope, category, key), value);
}
public static void Store(string category, string key, object value)
{
Store(Scope.Global, category, key, value);
}
public static void Store(Scope scope, string key, object value)
{
Store(scope, string.Empty, key, value);
}
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
):
#region Values retrieving (null if not found)
public static object Retrieve(Scope scope, string category, string key)
{
return SessionKey(scope, category, key);
}
public static object Retrieve(string category, string key)
{
return Retrieve(Scope.Global, category, key);
}
public static object Retrieve(Scope scope, string key)
{
return Retrieve(scope, string.Empty, key);
}
public static object Retrieve(string key)
{
return Retrieve(string.Empty, key);
}
#endregion
#region Values retrieving (with default value)
public static object RetrieveWithDefault(Scope scope,
string category, string key, object defaultValue)
{
object value = SessionKey(scope, category, key);
return value == null ? defaultValue : value;
}
public static object RetrieveWithDefault(string category,
string key, object defaultValue)
{
return RetrieveWithDefault(Scope.Global, category, key, defaultValue);
}
public static object RetrieveWithDefault(Scope scope,
string key, object defaultValue)
{
return RetrieveWithDefault(scope, string.Empty, key, defaultValue);
}
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):
#region Values clearing
public static void Clear()
{
HttpContext.Current.Session.Clear();
}
public static int ClearScope(Scope scope)
{
return ClearStartsWith(string.Format("{0}.", GetScopeHash(scope)));
}
public static int ClearCategory(Scope scope, string category)
{
return ClearStartsWith(string.Format("{0}.{1}.",
GetScopeHash(scope), category));
}
public static int ClearCategory(string category)
{
return ClearCategory(Scope.Global, category);
}
public static void Clear(Scope scope, string category, string key)
{
Store(scope, category, key, null);
}
public static void Clear(string category, string key)
{
Clear(Scope.Global, category, key);
}
public static void Clear(Scope scope, string key)
{
Clear(scope, string.Empty, key);
}
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:
- move the class code in to your solution
- use the Initia.Web project included in the sample project
- compile and reference the Initia.Web project’s assembly.
Once you’ve done this, you may use the Initia.Web
namespace like this:
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.
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:
...
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:
...
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:
...
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: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):
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
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):
#region Events handlers
private void btnAddUserHit_Click(object sender, EventArgs e)
{
int userHits = (int)SessionHelper.RetrieveWithDefault(
"User", "Hits", 0);
SessionHelper.Store("User", "Hits", userHits + 1);
UpdateUI();
}
private void btnClearGlobalCategory_Click(object sender, EventArgs e)
{
SessionHelper.ClearCategory(SessionHelper.Scope.Global, "User");
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):
#region User interface
public void UpdateUI()
{
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.