How do you count people visiting my site at any given time? This question, in different variations, comes up in newsgroups quite often. There's really no canned answer. One good way of doing it is to log all sessions in a database table, and give each session a reasonable timeout. The web application will need to update the timestamp of its respective session on each page request. For example, if a visitor didn't browse from page to page for 5...10 minutes it's quite reasonable to assume he/she is gone.
This is half of the problem. The other half is clean out expired sessions. It's actually a tricky one. For example, you could run an NT service and have it wake up, say, every 10 minutes, and purge expired sessions, but it sounds like too much hassle. Anyone who has ever written an NT service knows what a pain they are. Besides, your hosting company will refuse to run your NT service. You could write a SQL job to do the same, but again your hosting company most likely will not bother.
Just recently I saw a reasonable solution presented by Rob Howard at TechEd 2004. Download slides and sample code under Blackbelt ASP.NET Controls. As you unpack samples, you will find HttpModule.cs in the Blackbelt_AspNet_Demos\BlackbeltBLL folder.
Rob showed how to wire a database cache dependency with the help of an HttpModule. The code is simple yet elegant. My spin-off is listed below:
public class SessionPurger : IHttpModule
{
private static Timer timer;
private const int interval = 1000 * 60 * 10;
// ------------------------------------------------------
public void Init(HttpApplication application) {
// Wire-up application events
if (timer == null)
timer = new Timer(new TimerCallback(ScheduledWorkCallback),
application.Context, 0, interval);
}
// ------------------------------------------------------
private void ScheduledWorkCallback (object sender) {
HttpContext ctx = (HttpContext) sender;
DataProvider.Instance (ctx).PurgeExpiredSessions ();
}
// -------------------------------------------------------
public void Dispose() {
timer = null;
}
}
Implementation Details
An HttpModule fits the bill just right because it allows us to tap into the web application chain of events. We set up a timer and assign it a method to call at certain intervals. In my code ScheduledWorkCallback will be invoked every 10 minutes to purge expired sessions.
In ScheduledWorkCallback I call a stored procedure (via my Data Access Layer) which extends expiration of the current session by another 10 minutes. I track each session by its SessionID which is guaranteed to be unique. SessionIDs are 15 characters in length.
Gotcha #1: Threading
There's one very important thing to note here. Let me quote MSDN first:
Use a TimerCallback delegate to specify the method that is called by a Timer. This method does not execute in the thread that created the timer; it executes in a separate thread pool thread that is provided by the system. The TimerCallback delegate invokes the method once after the start time elapses, and continues to invoke it once per timer interval until the Dispose method is called.
Pay attention to the fact that ScheduledWorkCallback is called on a separate thread. I figured out the hard way that if you ever need to call HttpContext.Current it won't be there. That's why you coax it from the sender parameter.
HttpContext ctx = (HttpContext) sender;
Gotcha #2: Session IDs
If you store nothing in your Session a new SessionID is issued on each page request. If it happens the visitor stats are skewed really bad. You either need to store something in Session (not pretty) to trigger session state serialization or include a global.asax file in the project and make sure it has Session_Start in there. Presence of Session_Start does the trick. I always advocate dropping global.asax if you don't use it for anything, but this is one of those times when you need it.
Accuracy
A couple of words about accuracy. This solution ain't perfect. It's pretty good, but not 100% accurate. I don't know of a 100% accurate solution to this problem. But it comes pretty close.
Alternative Approaches
Why not use Session_OnEnd in global.asax? Session_OnEnd has too many what-ifs and is pretty unreliable. I wouldn't bank on it. It works only for InProc sessions. Also, it's a carryover from the classic ASP and is, essentially, a fudge.
I'm sure there other alternatives out there. Anyone?