The Trouble with Modules
Because I am bringing the story of Charlie up to date, I will occasionally get a few things out of order. I realise that I have done this when I said that the Web System included an IWebModule interface that mimicked the IHttpModule interface. In fact, I introduced the IWebModule interface because I had trouble with the intrinsic IHttpModule interface. Let me explain.
In order for the Plugins to fully participate in the processing of an incoming request, the Plugins needed to be able to hook into the events of the underlying HttpApplication pipeline. For example, the Security Plugin would need to be able to handle the AuthenticateRequest event and the AuthorizeRequest event. ASP.NET allows components to subscribe to the HttpApplication events by implementing the IHttpModule interface, and registering themselves in the Web.config file.
The IHttpModule interface is very simple, and looks like this:
public interface IHttpModule { void Init(HttpApplication application); void Dispose(); }
When the first request to an ASP.NET application comes in, ASP.NET creates a new instance of the HttpApplication class. (Already that misses a few subtleties, but we can ignore them for now.) ASP.NET then looks at the Web.config file, and checks whether any HttpModules have been registered. If so, it will create one instance of each module, and will pass the HttpApplication instance to the Init method above. This allows an HttpModule to subscribe to any of the events raised by HttpApplication. Here is an example of subscribing to the AuthenticateRequest event:
public class AuthenticationModule : IHttpModule { public void Init(HttpApplication application) { application.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest); } void OnAuthenticateRequest(Object sender, EventArgs e) { // we can now handle the AuthenticateRequest event } public void Dispose() { // No resources used. } }
One of the features of Charlie is that it is to serve multiple websites from a single installation. I wanted to allow one website to use, say, the Charlie.SecurityPlugin and another website to use, say, the Customer.SecurityPlugin. This means that I could not register the HttpModules in the Web.config file, but that I would have to install them dynamically after a request has begun. This must occur after the BeginRequest event, since that event is used to determine which website we are dealing with, and therefore which HttpModules to install.
The IHttpModule interface suggests that all you need to do to install a module is create a new instance of that module, and then pass in the current HttpApplication object to its Init method. This is precisely what ASP.NET does, so I figured I could do it too, if slightly later in the pipeline of events. Well, ASP.NET knows something that I don’t, because I could not get this to work, and I tried for hours. I could dynamically load an HttpModule during the initialization of the HttpApplication class, but not after the BeginRequest event had been fired. The events are still there, and there is no error when a dynamically-installed HttpModule subscribes to the events, but when the event fires it will not notify any HttpModules that happened to subscribe after BeginRequest.
After trying for hours to get HttpModules to be installed dynamically when an incoming request is received, I decided to mimic the IHttpModule interface with my own IWebModule interface. It looks exactly the same, except where the IHttpModule accepts an HttpApplication, the IWebModule accepts a WebApplication (which derives from HttpApplication anyway).
public interface IWebModule { void Init(WebApplication application); void Dispose(); }
I then added custom events to my WebApplication class that mimicked the events of the base HttpApplication class. When the underlying HttpApplication fires its AuthenticateRequest event, the WebApplication fires its DuringAuthenticateRequest event. When the underlying HttpApplication fires its PostAuthenticateRequest event, the WebApplication fires its AfterAuthenticateRequest event. This double firing of events allows an IWebModule to effectively subscribe the events of the HttpAppliction, by subscribing to an intermediate event.
This means that the above example of an AuthenticationModule needs to be changed ever-so-slightly:
public class AuthenticationModule : IWebModule { public void Init(WebApplication application) { application.DuringAuthenticateRequest += new EventHandler(this.OnDuringAuthenticateRequest); } void OnDuringAuthenticateRequest(Object sender, EventArgs e) { // we can now handle the AuthenticateRequest event } public void Dispose() { // No resources used. } }
This worked, and allowed Charlie to dynamically install modules that could handle the events of the ASP.NET pipeline. I was pleased with myself until I noticed that sometimes the modules were handling an event not once, but twice. In the example above, the OnDuringAuthenticateRequest method would sometimes execute two times. If I rebuilt the application, the first request would work properly. But every subsequent request would see the events being handled twice. For the smart bananas in the audience, that may have been the only clue they would need to recognise the likely problem. But this had me stumped. I resorted to using Visual Studio’s debugger, with which I am very unfamiliar. I could see that the modules were only subscribing once to an event, but that event would nonetheless call the module twice. I ended up writing a crude logger with which to record messages that would be written out to a text file at the end of each request.
I rebuilt Charlie, and made a page request. The log file showed that the test module subscribed once, and its event handler ran once. I then made a second page request. The log file showed that the test module subscribed once, but its event handler ran twice. This was exactly the problem that had me stumped. I then made a third request, and the log file showed that the test module subscribed once, but its event handler ran three times. After the fourth request, the event handler ran four times.
What I had forgotten is that unlike most objects, the HttpApplication object does not get marked for deletion at the end of a request. Rather, it gets returned to the application pool to be re-used. My code had not been clearing the events, so a given module would subscribe once to an event, but it would not be disposed of at the end of the request, as it was still subscribing to the event of an object that had returned to the application pool. During the next request, a second copy of that module would subscribe to the same event, and when that event fired it would run the event handler of the first module and then run the same event handler in the second module. During the next request, a third copy of that module would subscribe to the same event, and when the event fired it would run the event handler in the first, second, and now third copy of the same module.
The fix was simple. At the end of the request, clear the events of any subscribers. And that is why, if you ever look at the code for Charlie's WebApplication class, you will find the following comment in its EndRequest event handler:
private void OnEndRequest(Object sender, EventArgs e) { /* * IMPORTANT! * * This WebApplication instance returns to the pool. * So, we must dispose of all instance variables used. * * For example, if we do not explicitly kill the wired-up * event handlers, they will execute once during the first * request, twice during the second request, three times * during the third request, and so on. Remember this, * because it took fucking hours to debug that problem. * */ if (this.AfterBeginRequest != null) this.AfterBeginRequest = null; if (this.BeforeAcquireWebContext != null) this.BeforeAcquireWebContext = null; // and so on for each custom event. }
These two problems together took me a full day to overcome. But at the end of the day, I had a WebApplication class and a WebModule class that would allow the Plugins to become involved in every point in the ASP.NET pipeline. I had also learned that a Logging component should be one of the first things added to a new application.
by Alister Jones | Next up: The Trouble with Filters →
----
Post a Comment