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.
The problem
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:
- It is not type-safe: the type of an item stored in the
session is Object; you always need to cast it to its actual type
when you retrieve it. This cast is always based on the developer knowing
the actual type of the object stored for that key. If there is a design
change and the type of that item stored has changed, you MUST make sure
that all casts when retrieving the item are updated to the correct type,
otherwise you get runt-time cast exceptions.
- It is open to key name collisions: developers can use by
mistake the same key for two different purposes.
- It is open to key name mistypes: developers can by mistake
mistype the key and then the expected object will not be found in the
session.
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 solution
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:
- The design based on the Adapter pattern is less
convenient: you always have to pass in the ASP.Net Session (HttpSessionState)
to get your type-safe SessionAdapter. But is safer to use because by having
you pass the Session in, the call is guaranteed to be in the context of a
web request.
- The design based on the Façade pattern is more convenient:
you don't have to pass anything to get your type-safe SessionFacade
(it is just a static class implementation). But it assumes that it's being
called in the context of a web request – if not, the current HttpContext
will be null and you'll get run-time exceptions.
The Adapter pattern implementation
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;
}
}
}
}
Example of use:
{
//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;
}
The Façade pattern implementation
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;
}
}
}
}
Example of use:
{
//get the values
string user = SessionFacade.UserLogonName;
Nullable<DateTime> logon_timestamp = SessionFacade.UserLogonDateTime;
//set the values
SessionFacade.UserLogonName
= "George";
SessionFacade.UserLogonDateTime
= DateTime.Now;
}
Conclusion:
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:
- The usual benefits of having setters (e.g. validate what's
getting into the session; log the value if in DEBUG mode, etc).
- The usual benefits of having getters (e.g. initialize
value to a default if not present in the session, etc).
- They hide the internal implementation of the session
workings, therefore reducing the chances of errors in the application and
decoupling the application code from changes in the session's internal
workings.
- Allow for retrieval of value-types (such as the DateTime
in the examples above) or null if the value is not present through the use
of the generic Nullable<T> (available in .Net 2.0 only).
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.