asp.netiisevent-handlinghttpmoduleintegrated-pipeline-mode

Subscribing to HttpApplication events inside HttpModules behave differently between Integrated and Classic mode


I noticed some strange behavior regarding the events raised by the same HttpApplication instance and the HttpModules that subscribed the same eventhandler during their initialization, and this between IntegratedMode and ClassicMode in IIS 7 (at least up to version 8).

It seems that when your modules subscribe event handlers to the given HttpApplication instance in their Init() method, when running in IntegratedMode, that the C# rules related to subscribing to events don't apply anymore, at least when the events are being raised.

Normally when you do a subscription like this

httpApplication.EndRequest -= SomeMethod;
httpApplication.EndRequest += SomeMethod;

You are garantueed that SomeMethod is only subscribed once and therefore when the httpApplication's EndRequest is raised, your method will only be called once.

Of course if SomeMethod is an instance method and you have multiple instances, then each instance will have its method called, that is normal. But if you have a static method, then no matter how many different instances subscribe that same static SomeMethod it should only be called once.

And that does not seem to be the case when you have multiple HttpModule instances that subscribe the same static method to the same event of the same HttpApplication instance, while running in IntegratedMode

If you decompile the HttpApplication code then you see that each event subscription is actually translated to some notification in IIS (at least when running in IntegratedMode). That is all fine, but I have the feeling that they made the assumption that each event handler being attached to the HttpApplication instance events during the Init() method of the HttpModule is supposed to be an instance method of that specific HttpModule which is strange to say the least?

Below you'll find a reproducing sample as small as possible that clearly reflects this issue. I'm not looking for other ways to create the sample (to bypass the issue, restructure the code, ...), it simply reproduces the issue with the least amount of code and setup.

So my question is:

Is this strange behavior by design or is it something they overlooked/a bug? Or do I make the wrong assumptions with regard to the subscriptions?

Steps to reproduce

  1. Create an empty web application project named IssueDemo and include the following files into it

  2. Add an empty Index.aspx page (I'm using a webform page, but the same issue is there for MVC...)

  3. Add a Modules.cs file with the following content

    namespace IssueDemo
    {
        public abstract class ModuleBase : System.Web.IHttpModule
        {
            public void Init(System.Web.HttpApplication context)
            {
                System.IO.File.AppendAllText(
                    System.AppDomain.CurrentDomain.BaseDirectory + "trace.log",
                    string.Format("Init() called on {0} (#{1}) for HttpApplication (#{2}){3}",
                    this.GetType(),
                    this.GetHashCode(),
                    context.GetHashCode(),
                    System.Environment.NewLine));
    
                context.EndRequest -= LogSomething;
                context.EndRequest += LogSomething;
            }
    
            public void Dispose() { }
    
            private static void LogSomething(object sender, System.EventArgs e)
            {
                System.Web.HttpApplication httpApplication = (System.Web.HttpApplication)sender;
                System.IO.File.AppendAllText(
                    System.AppDomain.CurrentDomain.BaseDirectory + "trace.log",
                    string.Format("LogSomething() called on ModuleBase triggered by event raise of HttpApplication (#{0}) for Request (#{1}): {2}{3}",
                    httpApplication.GetHashCode(),
                    httpApplication.Request.GetHashCode(),
                    httpApplication.Request.RawUrl,
                    System.Environment.NewLine));
    
                httpApplication.Response.Write("Written by ModuleBase's LogSomething()<br/>");
            }
        }
    
        public class MyHttpModule : ModuleBase { }
        public class MyOtherHttpModule : ModuleBase { }
    }
    
  4. Adapt the web.config file to reflect the following content (beware of the assembly reference if you did not name your project IssueDemo)

    <?xml version="1.0"?>
    <configuration>
      <system.web>
        <compilation debug="true" targetFramework="4.5" />
        <httpRuntime targetFramework="4.5" />
        <httpModules>
          <add name="MyHttpModule" type="IssueDemo.MyHttpModule, IssueDemo"/>
          <add name="MyOtherHttpModule" type="IssueDemo.MyOtherHttpModule, IssueDemo"/>
        </httpModules>
      </system.web>
      <system.webServer>
        <validation validateIntegratedModeConfiguration="false" />
        <modules>
          <add name="MyHttpModule" type="IssueDemo.MyHttpModule, IssueDemo" />
          <add name="MyOtherHttpModule" type="IssueDemo.MyOtherHttpModule, IssueDemo" />
        </modules>
      </system.webServer>
    </configuration>
    
  5. Run it in ClassicMode (you can use the built-in VS Development Server or choose the Classic .Net AppPool in IIS for your application). You'll see the following message in the browser:

    Written by ModuleBase's LogSomething()
    

    and the trace.log file will show the following content (I removed the entries for favicon.ico and instance ids will differ):

    Init() called on IssueDemo.MyHttpModule (#9654443) for HttpApplication (#11543392)
    Init() called on IssueDemo.MyOtherHttpModule (#66322936) for HttpApplication (#11543392)
    LogSomething() called on ModuleBase triggered by event raise of HttpApplication (#11543392) for Request (#19612087): /index.aspx
    

    which is basically what I would expect when subscribing the same static method to the same HttpApplication instance (#11543392).

  6. Run it in IntgratedMode (you can't use the built-n VS Development Server but you can use IISExpress or normal IIS with DefaultAppPool). You'll now see the following messages in the browser:

    Written by ModuleBase's LogSomething()
    Written by ModuleBase's LogSomething()
    

    and the trace.log file will show the following content (I removed the entries for favicon.ico and the creation of other httpApplication instances, and instance ids will differ):

    Init() called on IssueDemo.MyHttpModule (#39086322) for HttpApplication (#36181605)
    Init() called on IssueDemo.MyOtherHttpModule (#28068188) for HttpApplication (#36181605)
    LogSomething() called on ModuleBase triggered by event raise of HttpApplication (#36181605) for Request (#63238509): /index.aspx
    LogSomething() called on ModuleBase triggered by event raise of HttpApplication (#36181605) for Request (#63238509): /index.aspx
    

    which is not what I expected when subscribing the same static method to the same HttpApplication instance (#36181605). You see the duplicate execution for the same request (#63238509), and since there is only event handler attached, the only conclusion I can make is that the event is raised twice. By the way if you add some more derived types and register them in the web.config, you'll see that the duplications will increase (but only in IntegratedMode).

If anybody can answer this, that would be great. In the meanwhile I already worked around this issue by checking if our code already executed during a specific request.


Solution

  • After a few days I asked the same question on Microsoft Connect and today they provided the following answer: This behavior is by design and deals with the difference in how modules are registered between classic mode and integrated mode

    Below you find the elaboration on their answer as well:

    In classic mode, IIS effectively sees the entire managed ASP.NET application (both the System.Web runtime and any custom modules which are registered) as one single HTTP module. IIS simply notifies ASP.NET that it should perform some work, and the ASP.NET runtime will go through its list of modules and kick everything off one by one. Since ASP.NET is entirely responsible for event management, we just use a single EventHandlerList (per HttpApplication) to coordinate everything.

    In integrated mode, however, IIS is aware of every single individual module running in the pipeline. IIS coordinates which modules are to receive which notifications. Once a module's Init method has run to completion, its registrations are considered "baked" for the lifetime of the module. This implies that the module event registrations are segregated: each module is given its own independent event handler registration object (since IIS internally keeps them segregated). If Module A calls add_EndRequest with some delegate and Module B calls remove_EndRequest with the same delegate, the event handler store is actually being backed by two different objects in memory, so Modules A and B cannot influence each others' registrations.