订阅HttpModules中的HttpApplication事件在集成模式和经典模式之间的行为有所不同

时间:2013-05-27 11:20:26

标签: asp.net iis event-handling httpmodule integrated-pipeline-mode

我注意到有关同一HttpApplication实例引发的事件以及在初始化期间订阅了相同的事件处理程序HttpModules的一些奇怪行为,并且这在之间IIS 7中的 IntegratedMode ClassicMode (至少版本为8)。

当您的模块在HttpApplication方法中为给定的Init()实例订阅事件处理程序时,似乎在 IntegratedMode 中运行时,与订阅事件相关的C#规则不再适用,至少在引发事件时是这样。

通常在您进行此类订阅时

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

您已经知道SomeMethod只订阅了一次,因此当httpApplication's EndRequest被提出时,您的方法只会被调用一次。

当然如果SomeMethod是一个实例方法并且你有多个实例,那么每个实例都会调用它的方法,这是正常的。但是如果你有一个静态方法,那么无论有多少个不同的实例订阅相同的静态SomeMethod,它只应该被调用一次。

当您有多个HttpModule个实例为相同的HttpApplication实例的同一事件订阅相同的静态方法时,情况似乎并非如此,而在 IntegratedMode

如果您反编译HttpApplication代码,那么您会看到每个事件订阅实际上都转换为IIS中的某些通知(至少在 IntegratedMode中运行时 )。这一切都很好,但我觉得他们假设在HttpApplication的{​​{1}}方法期间附加到Init()实例事件的每个事件处理程序应该是一个特定HttpModule的实例方法,至少可以说是奇怪的吗?

下面你会发现一个尽可能小的复制样本,它清楚地反映了这个问题。我不是在寻找其他方法来创建示例(绕过问题,重构代码,......),它只是用最少量的代码和设置重现问题。

所以我的问题是:

这是设计上的奇怪行为,还是他们忽略的东西/错误?或者我对订阅做出错误的假设?

重现的步骤

  1. 创建一个名为 IssueDemo 的空Web应用程序项目,并在其中包含以下文件

  2. 添加一个空的Index.aspx页面(我正在使用一个webform页面,但MVC存在同样的问题......)

  3. 添加包含以下内容的Modules.cs文件

    HttpModule
  4. 调整web.config文件以反映以下内容(如果您没有为项目命名,请注意程序集引用 IssueDemo

    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 { }
    }
    
  5. 在ClassicMode中运行它(您可以使用内置的VS Development Server或在IIS中为您的应用程序选择 Classic .Net AppPool )。您将在浏览器中看到以下消息:

    <?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>
    

    并且 trace.log 文件将显示以下内容(我删除了favicon.ico的条目,而实例 ID 将有所不同):

    Written by ModuleBase's LogSomething()
    

    这基本上是我在将相同的静态方法订阅到同一个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 实例时所期望的(#11543392)。

  6. 在IntgratedMode中运行它(您不能使用内置的VS开发服务器,但可以使用IISExpress或普通的IIS与 DefaultAppPool )。您现在将在浏览器中看到以下消息:

    HttpApplication

    trace.log 文件将显示以下内容(我删除了favicon.ico的条目和其他httpApplication实例的创建,而实例 ids 将有所不同):

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

    这不是我在将相同的静态方法订阅到同一个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 实例时所期望的(#36181605)。您看到同一请求的重复执行(#63238509),并且由于只附加了事件处理程序,我可以做的唯一结论是事件被引发两次。顺便说一下,如果你添加一些派生类型并在web.config中注册它们,你会发现重复会增加(但仅限于IntegratedMode)。

  7. 如果有人能回答这个问题,那就太好了。与此同时,我已经通过检查我们的代码是否已在特定请求中执行来解决此问题。

1 个答案:

答案 0 :(得分:4)

几天后我在Microsoft Connect上问了同样的问题,今天他们提供了以下答案:这种行为是设计上的,处理模块在经典模式和集成模式之间注册的方式不同

下面你会找到他们答案的详细说明:

  

在经典模式下,IIS有效地将整个托管ASP.NET应用程序(System.Web运行时和已注册的任何自定义模块)视为一个单独的HTTP模块。 IIS只是通知ASP.NET它应该执行一些工作,并且ASP.NET运行时将遍历其模块列表并逐个完成所有操作。由于ASP.NET完全负责事件管理,我们只使用一个EventHandlerList(每个HttpApplication)来协调所有事情。

     

但是,在集成模式下,IIS了解管道中运行的每个单独模块。 IIS协调哪些模块将接收哪些通知。一旦模块的Init方法运行完成,其注册在模块的生命周期内被视为“烘焙”。这意味着模块事件注册是隔离的:每个模块都有自己独立的事件处理程序注册对象(因为IIS内部保持它们隔离)。如果模块A使用某个委托调用add_EndRequest,而模块B使用相同的委托调用remove_EndRequest,则事件处理程序存储实际上由内存中的两个不同的对象支持,因此模块A和B不能相互影响注册