3

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

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

通常当您进行这样的订阅时

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

保证您SomeMethod只订阅一次,因此当httpApplication's EndRequest引发时,您的方法只会被调用一次。

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

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

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

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

所以我的问题是:

这种奇怪的行为是设计使然还是他们忽略了/错误?还是我对订阅做出了错误的假设?

重现步骤

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

  2. 添加一个空的 Index.aspx 页面(我使用的是网络表单页面,但 MVC 也存在同样的问题......)

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

    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. 调整 web.config 文件以反映以下内容(如果您没有将项目命名为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. 在 ClassicMode 下运行它(您可以使用内置的 VS Development Server 或为您的应用程序选择 IIS 中的Classic .Net AppPool)。您将在浏览器中看到以下消息:

    Written by ModuleBase's LogSomething()
    

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

    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
    

    这基本上是我在为同一个HttpApplication实例订阅相同的静态方法时所期望的(#11543392)。

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

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

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

    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
    

    当为同一个实例订阅同一个静态方法时,这不是我所期望的HttpApplication(#36181605)。您会看到同一请求的重复执行 (#63238509),并且由于仅附加了事件处理程序,因此我可以得出的唯一结论是该事件被引发了两次。顺便说一句,如果您添加更多派生类型并在 web.config 中注册它们,您会看到重复项会增加(但仅在 IntegratedMode 中)。

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

4

1 回答 1

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 不能影响彼此的注册。

于 2013-06-18T14:32:28.467 回答