Building a type-safe ASP.Net session
By George Mihaescu
Summary: this article makes the case for using a type safe session object instead of the standard ASP.Net Session and presents two solutions for this, one based on the Adapter design pattern, the other based on the Façade design pattern.
A similar case can be made for the ASP.Net cache – that is presented in another article.
ASP.Net offers the Session object for application that need to store user session data (i.e. data across requests made by the same web client). However, the provided Session has a number of shortcomings:
By the way, none of these shortcomings are flaws in the ASP.Net Session design: the Session is a generic "bag" of any data types you may need to store. ASP.Net designers cannot know what each application will need to store in the session – however, you do, because it's your application, and below you'll find what you can do to build a better Session for you particular application needs.
The goal is to find a design that would resolve the shortcomings listed above. I have found two such designs that only have minor differences:
using System;
using System.Web;
using System.Web.SessionState;
/// <summary>
/// SessionAdapter implements an Adapter pattern over the ASP.Net
/// HttpSessionState in order to achieve type-safety and consistency.
/// </summary>
public class SessionAdapter
{
// Keys used for values in the session
private const string USER_LOGON_NAME = "USER_LOGON_NAME";
private const string USER_LOGON_TIME = "USER_LOGON_TIME";
private readonly HttpSessionState m_session;
private SessionAdapter(HttpSessionState session)
{
m_session = session;
}
/// <summary>
/// Factory, gets a session adapter
/// </summary>
public static SessionAdapter GetSessionAdapter(HttpSessionState session)
{
return new SessionAdapter(session);
}
/// <summary>
/// Sets / gets the current user's logon name.
/// May return null if value not in session.
/// Pass in null to remove the object from session.
/// </summary>
public string UserLogonName
{
get { return m_session[USER_LOGON_NAME] as string; }
set
{
if (value == null)
{
m_session.Remove(USER_LOGON_NAME);
}
else
{
m_session[USER_LOGON_NAME] = value;
}
}
}
/// <summary>
/// Sets / gets the current user's logon date and time
/// May return null if value not in session.
/// Pass in null to remove the object from session.
/// </summary>
public Nullable<DateTime> UserLogonDateTime
{
get
{
//retrieve the object from the session
object val = m_session[USER_LOGON_TIME];
if (val == null)
{
return null;
}
else
{
return (DateTime)val;
}
}
set
{
//if passed value is null, remove object from session
if (value == null)
{
m_session.Remove(USER_LOGON_TIME);
}
else
{
m_session[USER_LOGON_TIME] = value;
}
}
}
}
{
//get the session adapter
SessionAdapter s = SessionAdapter.GetSessionAdapter(Session);
//get the values
string user = s.UserLogonName;
Nullable<DateTime> logon_timestamp = s.UserLogonDateTime;
//set the values
s.UserLogonName = "George";
s.UserLogonDateTime = DateTime.Now;
}
using System;
using System.Web;
using System.Web.SessionState;
/// <summary>
/// SessionFacade implements a Facade pattern over the ASP.Net
/// HttpSessionState in order to achieve type-safety and consistency.
/// </summary>
public static class SessionFacade
{
//Key names used in the session
private const string USER_LOGON_NAME = "USER_LOGON_NAME";
private const string USER_LOGON_TIME = "USER_LOGON_TIME";
/// <summary>
/// Sets / gets the current user's logon name.
/// May return null if value not in session.
/// Pass in null to remove the object from session.
/// </summary>
public static string UserLogonName
{
get
{
//retrieve the session from the context
HttpSessionState s = HttpContext.Current.Session;
//retrieve the object from the session
return s[USER_LOGON_NAME] as string;
}
set
{
//retrieve the session from the context
HttpSessionState s = HttpContext.Current.Session;
//if passed value is null, remove object from session
if (value == null)
{
s.Remove(USER_LOGON_NAME);
}
else
{
s[USER_LOGON_NAME] = value;
}
}
}
/// <summary>
/// Sets / gets the current user's logon date and time
/// May return null if value not in session.
/// Pass in null to remove the object from session.
/// </summary>
public static Nullable<DateTime> UserLogonDateTime
{
get
{
//retrieve the session from the context
HttpSessionState s = HttpContext.Current.Session;
//retrieve the object from the session
object val = s[USER_LOGON_TIME];
if (val == null)
{
return null;
}
else
{
return (DateTime)val;
}
}
set
{
//retrieve the session from the context
HttpSessionState s = HttpContext.Current.Session;
//if passed value is null, remove object from session
if (value == null)
{
s.Remove(USER_LOGON_TIME);
}
else
{
s[USER_LOGON_TIME] = value;
}
}
}
}
{
//get the values
string user = SessionFacade.UserLogonName;
Nullable<DateTime> logon_timestamp = SessionFacade.UserLogonDateTime;
//set the values
SessionFacade.UserLogonName = "George";
SessionFacade.UserLogonDateTime = DateTime.Now;
}
The above implementations are equally good; the chances that you'll be mistakenly using the SessionFacade outside the context of a web request (e.g. worker thread, unit test, or certain functions in Global.asax) are pretty slim, and if you do, you'll find out during development very quickly – therefore I would be inclined to use the implementation based on the Façade pattern (for its convenience).
Note that in addition to meeting the goals I've outlined at the beginning of the article, those implementations also offer additional benefits:
One minor objection to the above implementations can be that they don't prevent a sloppy / unaware developer in the team from circumventing them and going directly to the ASP.Net Session, bringing back in the issues listed at the top of the article.
If you want to prevent this, one possible solution could be to add code in the above implementations that checks the state of the underlying ASP.Net Session every time an item is set / retrieved in the Session (i.e. at the top of each get / set property implementation).
For instance, traverse all the keys and if you find one that's not a known key, throw an exception – that way you'll know immediately if someone has bypassed your type-safe implementation. But probably that's too heavy-handed for most cases.