Introduction
The Session_End
event is a useful event which an be handled in Global.asax to perform any actions when a session ends, such as logging an activity to the database, cleaning up temporary session files, etc.
However, when using any kind of state management other than InProc (such as StateServer
or SqlStateServer
), the ASP.NET web application does not fire the Session_End
event, and any code in this method will not be executed.
Background
Some browsing around returned a couple of good articles. The article Page tracking in ASP.NET offers a similar solution, though is geared around page tracking whereas my requirement was simply to find an alternative to the Session_End
event that would work with the ASP.NET StateServer.
There's another excellent article called Preventing Multiple Logins in ASP.NET This is where I got the idea of using the application cache with a sliding expiry to trigger an event when the session ends.
How it works
The SessionEndModule
class hooks into the PreRequestHandlerExecute
event and inserts/replaces an item in the application cache, with a sliding expiry equal to the session expiry, and a callback method to be called when the item is removed from the application cache. The key of the cache item is the SessionId
, and the value is the value of the item in the session with the key set as the SessionObjectKey
.
When the item expires and the callback method is called. The key, value and reason of the expired item is passed to this callback method. The key is the SessionId
, the value is the value copied from the session, and the reason is why the item was removed from the cache (was it removed, expired, underused, or it's dependency changed).
The callback method then checks that the item was removed as a result of it expiring, wraps the values into a SessionEndEventArgs
class (which exposes SessionId
and SessionObject
properties), and fires the SessionEnd event.
Using the code
The code consists of a HttpModule
called SessionEndModule
, which needs to be included in the project via the web.config file. It exposes a static property named SessionObjectKey
and a static event named SessionEnd
which will be fired when a session ends. The value of the SessionObjectKey
will be returned in the event arguments for the SessionEnd event.
First lets set up web.config
<httpModules>
<add name="SessionEndModule" type="SessionTestWebApp.Components.SessionEndModule, SessionTestWebApp"/>
</httpModules>
-->
<sessionState mode="StateServer" stateConnectionString="tcpip=127.0.0.1:42424" timeout="1" cookieless="false"/>
Then we need set up Global.asax
protected void Application_Start(object sender, EventArgs e)
{
SessionEndModule.SessionObjectKey = "UserEmail";
SessionEndModule.SessionEnd += new SessionEndEventHandler(SessionTimoutModule_SessionEnd);
}
Then we need to create our event handler method for the SessionEnd
event:
private static void SessionTimoutModule_SessionEnd(object sender, SessionEndedEventArgs e)
{
Debug.WriteLine("SessionTimoutModule_SessionEnd : SessionId : " + e.SessionId);
object sessionObject = e.SessionObject;
string val = (sessionObject == null) ? "[null]" : sessionObject.ToString();
Debug.WriteLine("Returned value: " + val);
}
In this demo, lets also wire up the Session_Start
and put some test data in the session
protected void Session_Start(object sender, EventArgs e)
{
Debug.WriteLine("Session started: " + Session.SessionID);
Session["UserId"] = new Random().Next(1, 100);
Session["UserEmail"] = new Random().Next(100, 1000).ToString() + "@domain.com";
Debug.WriteLine("UserId: " + Session["UserId"].ToString() + ", UserEmail: " +
Session["UserEmail"].ToString());
}
Testing the code
Start this project in debug mode and keep an eye on the output window. When the session starts, the session contents will populated with a random UserId
and UserEmail
. The session will end after approximately 1 minute, and fire the SessionEnd event, which will execute the SessionTimoutModule_SessionEnd
method and print the SessionId
of the session that ended and the UserEmail
it contained.
Returning more than one session value
The HttpModule.SessionEnd
event only supports returning the value of a single session variable. If you need to return more than one value, the easiest way is to create a serializable class, with properties for all your values, and store that in the session instead.
For example:
[Serializable]
public class SessionInfo
{
public string UserId;
public string UserName;
public string UserEmail;
}
SessionInfo sessionInfo = new SessionInfo();
sessionInfo.UserId = 10;
sessionInfo.UserName = "Bob Jones";
sessionInfo.UserEmail = "bobjones@company.com";
Session["SessionInfo"] = sessionInfo;
In Global.asax, you would then set the SessionObjectKey to 'SessionInfo':
SessionEndModule.SessionObjectKey = "SessionInfo";
You can then access this in the SessionEnd event:
private static void SessionTimoutModule_SessionEnd(object sender, SessionEndedEventArgs e)
{
Debug.WriteLine("SessionTimoutModule_SessionEnd : SessionId : " + e.SessionId);
SessionInfo sessionInfo = e.SessionObject as SessionInfo;
if (sessionObject == null)
{
Debug.WriteLine("Returned value is null");
}
else
{
Debug.WriteLine("Returned values - UserId: " + sessionInfo.UserId + ", UserName: " +
sessionInfo.UserName + ", UserEmail: " + sessionInfo.UserEmail);
}
}
Known limitations
As the module hooks into the PreRequestHandler
event, the value of the SessionObjectKey
stored in the application cache before the page executes. This means that if you change the session variable being returned by the module in the SessionEnd
event (or the SessionInfo
object) on a page, and then the session times out, the SessionEnd
event will contain the old value. It's possible to change this to use the PostRequestHandler
event, though I've experienced some strange behaviour with this, especially when using Response.TransmitFile.
This code also won't work on scenerarios where you are using a server farm. In the article 'Preventing Multiple Logins in ASP.NET', Peter Bromberg discusses this problem in more depth and offers some ideas for working around this.
History
Version 1.0 - 02 November 2007