ASP.NET 2.0 HTTPModules and EPiServer fun and games
Part of the TMCore EPiServer Module functionality is delivered by hooking events generated by EPiServer. This can be done in a couple of ways, originally we hooked then in the application start phase of the Global.aspx.cs file; however this was a message because it meant that customers had to recompile that file in order for it to work). A far cleaner solution, recommended by the extremely knowledgable Steve Celius was to use an HTTP Module.
HTTP Modules provide exceptionally useful functionality. It published events representing different phases of the application lifecycle that you can hook in to add your own code. Then register your handler via web.config and everything is coolio.
That's when we starting seeing some odd behaviour :)
What we did
Having looked around for various examples of how to implement an HTTP Module we settled on hooking the Init event to register our event handlers; then because we're neat, hook the Dispose event to deregister them. We rapidly found, however, that alone was not enough; simply because init may be called multiple times, and possibly by different threads concurrently.
So we followed the following pattern:
class EPiEventsHttpModule: IHttpModule
{
private static readonly object mutexLock = new Object();
private static bool isRegistered = false;
public void Init(HttpApplication app)
{
if (!isRegistered)
{
lock (mutexLock)
{
if (!isRegistered)
{
RegisterEventHandlers();
}
}
}
}
public void Dispose()
{
lock (EPiEventsHttpModule.mutexLock)
{
if (EPiEventsHttpModule.isRegistered)
{
DeregisterEventHandlers();
}
}
}
}
(obviously some code ommitted for clarity here)
This code worked fine under ASP.NET 1.1, however, we started getting reports from our beta customers that under ASP.NET 2.0, they were seeing that events were being fired multiple times. We eventually tracked this down to the fact that multiple event registrations had occurred. For example, we have an event handler that creates a new topic in the topic map when an EPiServer page is created, what we saw that was multiple topics were being created for each page (although because topic maps cannot have 2 topics with the same subject identifier, TMCore was magically merging them for us, resulting in multiple names and occurrences.
The Problem
After some considerable debugging effort we discovered how this seemingly impossible situation could occur. In ASP.NET 1.1 when an unexpected exception propagated up through the ASP.NET layers the exception would be thrown back to the user (if so configured). However, the application would continue on quite happily. However, in ASP.NET 2.0 this behaviour changed, and when uncaught exceptions mad eit up to the top level the application instance would be disposed of, however the Dispose() method of the HTTP Module would not get called. It just quietly gets rid of it. However, what it doesn't do, which is what confused me, is that it doesn't restart the application in a separate heap area. This means that the old HTTP Module handler is still registered.
The EPiServer.Global event handler area is static, and it's persisting even when the application is shut down and restarted [to those Java/J2EE programmers out there: yes, I know, I thought the same thing :-) ]. So, when the next application request comes in, ASP.NET realizes it needs to initialize a new application, and it re-runs the Init method on the HTTP Module. So, bingo, multiple event registrations
The solution
Given that Dispose() is now, to me, considered useless; we can only rely on the Init() method being called. Because .NET event handlers don't provide any kind of feedback (i.e. "was my registration successful") and I can't find a way to query it (i.e. "is this event handler already registered?") it does mean it's a bit of a challenge.
However, fortunately, because there's no feedback on event registration or deregistration; we can move the event deregistration from Dispose() in to the Init() method. One the first load, the CLR won't object to us demanding the deregistration of an non-registered handler, it just silent ignores it; so now we're safe to register. Because deregistration always before any registration, we can be sure that there's only 1 instance of the event handlers registered at any one point, thus solving our problem.
Here's the Init() method from the EPiServer Module so you can see exactly how we solved this:
public void Init(HttpApplication app)
{
if (!isRegistered)
{
lock (regMutexLock)
{
if (!isRegistered)
{
DeregisterEventHandlers();
RegisterEventHandlers();
}
}
}
else
{
if (log.IsInfoEnabled)
{
log.Info("Event handlers already registered, will not re-register them (sequential request)");
}
}
}