Reference:http://www.codeproject.com/KB/aspnet/SessionEndStatePersister.aspx
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.
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.
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.
//Code
using
System;
using
System.Diagnostics;
using
System.IO;
using
System.Web;
using
System.Web.Caching;
using
System.Web.SessionState;
namespace
SessionTestWebApp
{
///
<summary>
///
When an ASP.NET State Server other than InProc, the Session_End event
///
does not get fired. This is an HttpModule which uses some workarounds
///
and fires a static event when a session ends, with the value of a single
///
configurable session variable in the event arguments.
///
</summary>
public
class
SessionEndModule : IHttpModule
{
#region
Private Variables
private
HttpApplication m_HttpApplication;
private
static
string
m_SessionObjectKey;
#endregion
#region
Accessors
///
<summary>
///
This is the key of the item in the session which should be returned
///
in the SessionEnd event (as the SessionObject).
///
</summary>
///
<example>
///
If you're storing the user ID in the session, under a key called 'UserId'
///
and need to do something with it in the SessionEnd event, you would set
///
this to 'UserId', which would cause the value of the session key called
///
'UserId' to be returned.
///
</example>
public
static
string
SessionObjectKey
{
get
{
return
m_SessionObjectKey;
}
set
{
m_SessionObjectKey
=
value;
}
}
#endregion
#region
IHttpModule Implementation
public
void
Init(HttpApplication context)
{
m_HttpApplication
=
context;
m_HttpApplication.PreRequestHandlerExecute
+=
new
EventHandler(OnPreRequestHandlerExecute);
}
public
void
Dispose()
{
//
Do Nothing
}
#endregion
#region
Events
///
<summary>
///
Event raised when the session ends
///
</summary>
public
static
event
SessionEndEventHandler SessionEnd;
#endregion
private
void
OnPreRequestHandlerExecute(
object
sender, EventArgs e)
{
//
We only want to update the session when an ASPX page is being viewed
//
We're also doing this in the PreRequestHandler, as doing it elsewhere
//
(like the PostRequestHandler) can cause some strange behaviour.
if
(Path.GetExtension(m_HttpApplication.Context.Request.Path).ToLower()
==
"
.aspx
"
)
{
//
Ensure we have a HttpContext
if
(HttpContext.Current
==
null
)
{
Debug.WriteLine(
"
No current http context
"
);
return
;
}
//
Get the current session
HttpSessionState currentSession
=
HttpContext.Current.Session;
//
Ensure we have a current session
if
(currentSession
==
null
)
{
Debug.WriteLine(
"
No current session
"
);
return
;
}
//
Get the session timeout
TimeSpan sessionTimeout
=
new
TimeSpan(
0
,
0
, currentSession.Timeout,
0
,
0
);
//
Get the object in the session we want to retrieve when the session times out
object
sessionObject
=
currentSession[SessionObjectKey];
//
Add the object to the cache with the current session id, and set a cache removal callback method
HttpContext.Current.Cache.Insert(currentSession.SessionID, sessionObject,
null
, DateTime.MaxValue, sessionTimeout, CacheItemPriority.NotRemovable,
new
CacheItemRemovedCallback(CacheItemRemovedCallbackMethod));
}
}
///
<summary>
///
This method is fired when an item is removed from the cache. It is used to detect when a cache item
///
expires, indicating that the session has expired, and fires the SessionEnd event.
///
</summary>
private
void
CacheItemRemovedCallbackMethod(
string
key,
object
value, CacheItemRemovedReason reason)
{
if
(reason
==
CacheItemRemovedReason.Expired)
{
if
(SessionEnd
!=
null
)
{
SessionEndedEventArgs e
=
new
SessionEndedEventArgs(key, value);
SessionEnd(
this
, e);
}
}
}
}
///
<summary>
///
Event handler for handling the SessionEnd event
///
</summary>
public
delegate
void
SessionEndEventHandler(
object
sender, SessionEndedEventArgs e);
///
<summary>
///
SessionEndedEventArgs for use in the SessionEnd event
///
</summary>
public
class
SessionEndedEventArgs : EventArgs
{
public
readonly
string
SessionId;
public
readonly
object
SessionObject;
public
SessionEndedEventArgs(
string
sessionId,
object
sessionObject)
{
SessionId
=
sessionId;
SessionObject
=
sessionObject;
}
}
}
//Web.config
<
httpModules
>
<
add name
=
"
SessionEndModule
"
type
=
"
SessionTestWebApp.Components.SessionEndModule, SessionTestWebApp
"
/>
</
httpModules
>
<!--
Use the state server (rather than InProc), and
set
the timeout to
1
minute
for
easier testing
-->
<
sessionState mode
=
"
StateServer
"
stateConnectionString
=
"
tcpip=127.0.0.1:42424
"
timeout
=
"
1
"
cookieless
=
"
false
"
/>
//Global.asx
protected
void
Application_Start(
object
sender, EventArgs e)
{
//
In our sample application, we want to use the value of Session["UserEmail"] when our session ends
SessionEndModule.SessionObjectKey
=
"
UserEmail
"
;
//
Wire up the static 'SessionEnd' event handler
SessionEndModule.SessionEnd
+=
new
SessionEndEventHandler(SessionTimoutModule_SessionEnd);
}
private
static
void
SessionTimoutModule_SessionEnd(
object
sender, SessionEndedEventArgs e)
{
Debug.WriteLine(
"
SessionTimoutModule_SessionEnd : SessionId :
"
+
e.SessionId);
//
This will be the value in the session for the key specified in Application_Start
//
In this demonstration, we've set this to 'UserEmail', so it will be the value of Session["UserEmail"]
object
sessionObject
=
e.SessionObject;
string
val
=
(sessionObject
==
null
)
?
"
[null]
"
: sessionObject.ToString();
Debug.WriteLine(
"
Returned value:
"
+
val);
}
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());
}