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:

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 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:

 

 

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.