Recently, while working on some code for an ASP.NET project at work. We needed a tracking util to take basic metrics on user activity (page hit count etc) we would track them in Session
, then save the data to DB via Session_End
in Global.asax
.
I began hacking away, the initial code worked fine, updating the DB on each page load. I wanted to remove this DB hit on each request though and just rely on Session_End
to store all the data.
All of the tracking code is encapsulated in the Tracker
class, including properties that essentially wrap the Session variables.
The problem is that when I executed Tracker.Log()
in the Session_End
method, the HttpContext.Current.Session
in the Tracker code was failing with a NullReferenceException
. Now, this makes sense since HttpContext
always relates to the current request, and of course in Session_End
, there is no request.
I know that Global.asax
has a Session
property which returns a HttpSessionState
that actually seems to work fine (I ended up injecting it in to the tracker)..
But I am curious, how the hell can I get the same reference to the HttpSessionState
object used by Global.asax
from outside of Global.asax
?
Thanks in advance guys, I appreciate the input. :)
To answer the original question better:
Every single page request spins up a new Session
object and then inflates it from your session store. To do this, it uses the cookie provided by the client or a special path construct (for cookieless sessions). With this session identifier, it consults the session store and deserializes (this is why all providers but InProc need to be Serializable) the new session object.
In the case of the InProc provider, merely hands you the reference it stored in the HttpCache
keyed by the session identifier. This is why the InProc provider drops session state when the AppDomain
is recycled (and also why multiple web servers cannot share InProc session state.
This newly created and inflated object is stuck in the Context.Items
collection so that it's available for the duration of the request.
Any changes you make to the Session
object are then persisted at the end of the request to the session store by serializing (or the case of InProc, the HttpCache
entry is updated).
Since Session_End
fires without a current request in-fly, the Session
object is spun up ex-nilo, with no information available. If using InProc session state, the expiration of the HttpCache
triggers a callback event into your Session_End
event, so the session entry is available, but is still a copy of what was last stored in the HttpContext.Cache
. This value is stored against the HttpApplication.Session
property by an internal method (called ProcessSpecialRequest
) where it is then available. Under all other cases, it internally comes from the HttpContext.Current.Session
value.
Since the Session_End always fires against a null Context, you should ALWAYS use this.Session in that event and pass the HttpSessionState object down to your tracing code. In all other contexts, it's perfectly fine to fetch from HttpContext.Current.Session
and then pass into the tracing code. Do NOT, however, let the tracing code reach up for the session context.
Don't use Session_End
unless you know that the session store you are using supports Session_End
, which it does if it returns true
from SetItemExpireCallback
. The only in-the-box store which does is the InProcSessionState
store. It is possible to write a session store that does but the question of who will process the Session_End
is kind of ambiguous if there are multiple servers.