🤯 50% Off! 700+ courses, assessments, and books

Catching Session Timeouts before they Bite

Wyatt Barnett
Share

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.