Catching Session Timeouts before they Bite

Using sessions to store user information is a very handy feature of ASP.NET. But it is not without its dark side. One of the principal difficulties is the fact that sessions can die unexpectedly. This can happen for a number of reasons. It could just time out. The server could decide it needs to recycle the application. A user could clear his session cookies. There could be a sunspot.

In mild cases this can just cause some “Object Reference not set to Instance of Object” errors. In bad cases it can lead to data corruption as one attempts to update the underlying data store based on non-existent data. In any case, it can be headed off using a little bit of code.

The strategy is this: first, we need to create a SessionTimeoutWatcher class that, well, watches for timeouts. This class needs a few pieces of information to operate—namely the current session ID, the session ID of the preceding request, and access to some page-level variables and events.

To abstract the mechanics of storing and retrieving Session IDs, we will create a little interface:


public interface ISessionContainer
{
	/// <summary>
	/// Identifying string of the current requests' Session.
	/// </summary>
	string CurrentSessionId { get; set;}
	/// <summary>
	/// Identifying string of the previous requests' Session.
	/// </summary>
	string PreviousSessionId { get; set; }
	/// <summary>
	/// Flag determining if exceptions will be thrown upon Session timeout.
	/// </summary>
	bool EnforceSessionWatch { get; }
}

I also added a flag to allow the container to determine if the session watch is actually enforced.

Next, we need the watcher itself:


public class SessionTimeoutWatcher
{
	private ISessionContainer sessionContainer;
	private Page page;

	/// <summary>
	/// Initializes and sets up the Session Timeout watcher.
	/// </summary>
	/// <param name="sessionContainer">Container session data used by watcher</param>
	/// <param name="currentPage">Page requested.</param>
	public SessionTimeoutWatcher(ISessionContainer sessionContainer, Page currentPage)
	{
		page = currentPage;
		this.sessionContainer = sessionContainer;
		page.Load += new EventHandler(setupSessionWatch);
		page.LoadComplete += new EventHandler(checkSession);
	}

	private void setupSessionWatch(object sender, EventArgs e)
	{
		sessionContainer.PreviousSessionId = sessionContainer.CurrentSessionId;
		sessionContainer.CurrentSessionId = page.Session.SessionID;
	}

	private void checkSession(object sender, EventArgs e)
	{
		//Make sure we must enforce this logic.
		if (sessionContainer.EnforceSessionWatch)
		{
			//Check the session ids to see if they match.
			//If previous ID is null or empty, it means the session is new, which is OK.
			if (
				!string.IsNullOrEmpty(sessionContainer.PreviousSessionId)
				&&
				sessionContainer.CurrentSessionId != sessionContainer.PreviousSessionId
			   )
			{
				throw new SessionExpiredException("Session ID has changed.");
			}
			//Check if the session has been reset.
			if (
				sessionContainer.CurrentSessionId == sessionContainer.PreviousSessionId
				&&
				page.Session.IsNewSession
			   )
			{
				throw new SessionExpiredException("Session has been reset.");
			}
		}
	}
}

The above is not too fancy. It just takes a reference to the ISessionContainer and the current Page in the constructer, then wires itself into a few page events.

The key method is the checkSession(object, EventArgs) method which handles the actual check. The check is twofold. The first if statement makes sure that the session id has not changed. This would indicate a user cleared their cookies, for example. The second checks the Session’s IsNewSession flag to determine if the session has recycled but has the same ID.

If the check is invalid, it throws a custom Exception to tell the program that the Session has expired. The application can then determine what, if anything, to do, using the page- or application-level error handlers.

Finally, wiring it up on a page is very simple. The best place to add this, and many other global functions, is a base page class shared by all the pages in the application.


public class PageBase : Page, ISessionContainer
{
    public PageBase()
    {
        timeoutWatcher = new SessionTimeoutWatcher(this, this);
    }

    private SessionTimeoutWatcher timeoutWatcher;

    #region ISessionContainer Members

    public string CurrentSessionId
    {
        get
        {
            return (string)ViewState["__CurrentSessionId"];
        }
        set
        {
            ViewState["__CurrentSessionId"] = value;
        }
    }

    public string PreviousSessionId
    {
        get
        {
            return (string)ViewState["__PreviousSessionId"];
        }
        set
        {
            ViewState["__PreviousSessionId"] = value;
        }
    }

    public virtual bool EnforceSessionWatch
    {
        get 
        {
            return true;
        }
    }

    #endregion
}

One thing to note here—the EnforceSessionWatch method has been very intentionally declared as virtual. This allows a developer to easily override it on a given page to skip the session enforcement.

Enjoy & kick it if you like it.

Finally, you can download the a sample project containing all code used above from here.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • interpeo

    Thanks Wyatt!
    Great article, I will adopte your solution as soon as possible.
    interpeo

  • http://webhosting.uk.com/ James-A

    Nice article, thanks for sharing it.

  • Kumar

    An exception of type ‘SessionExpiredException’ occurred in WWB.SessionTimeout.DLL but was not handled in user code

    Additional information: Session has been reset.

    I got this Exception please guide me to use this code

  • wwb_99

    That exception is intentionally thrown when a session is reset.

    In fact, that exception is the point of the whole excercise. Basically, that exception allows you, the developer, to know that the session has been reset and that you should take appropriate action.

    It is easy to trigger in development, because rebuidling the website causes the session to be reset. You should manually browse to a page to start any debugging session to avoid the error.

  • Jean-Philippe Mailhot

    hi,
    thanx for sharing your Session Watcher solution! I have a question. How can you keep the CurrentSessionID from page to page, knowing that the viewstate is reseted as soon as a new page is loaded.
    Thank you!

  • wwb_99

    Interesting question, though a bit immaterial.

    Session timeouts will happen on a single page as a user, say, leaves the page open while they have a three martini lunch. Then they come back and click on a button. If a user is traversing pages, then their session will be active by default as they are interacting with the application.

  • Sandro

    The sample project is not available anymore :(

  • mdolivera

    Is it possible to get a link to the sample code again? I’m having a hard time implememnting the code above which is exactly what I need in my project.

    Thanks

  • Prakash

    Is it possible to share the project code again. The link is not working. Thanks in advance.

  • wwb_99

    I’ll see what I can do–this was posted about 3 home computers ago, so I can’t say for certain if the code sample is still on some drive somewhere.

  • KK

    I tried using your code. I have a page(Form1.aspx) with a some session variables. This Page derives from the PageBase in your code. The Session Timeout occurs and the Session_OnEnd event fires which should since the session timed out.My Form1.aspx gets displayed, there is a button, the buttonclick takes the user to Form.aspx which also derives from PageBase, so in that case the Check “checkSession” always seems to Pass although the Session has timedout. Can you please guide me what am I doing wrong and also post your sample project so that it will help me and many others.
    Thanks.Waiting for your reply.

  • wwb_99

    I can’t really say without seeing the code, but what are you doing in the Session_OnEnd event handler?

    PS: Also, are you doing anything that would be catching all exceptions in this code? That could eat the SessionExpiredException, kind of nullifying the purpose.

  • VB-Artist

    Please help,

    I tried downloading the sample project, but I keep getting file not found. This is a great article. So if someone could re-post the WWb.Sessiontimeout.zip or post an example of the code used in an aspx. Thanks