Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

How do ASP.NET Application_ Events Work


:P
On this page:

I got an interesting question via email today that asked the question:

“How do ASP.NET Application_ Event Handlers get hooked so that they are automatically fired?”

If you’ve worked with ASP.NET for any amount of time you probably know about the global.asax file and its backing global class that holds event handlers for several common HttpApplication Event Handlers:

public class Global : System.Web.HttpApplication
{

    protected void Application_Start(object sender, EventArgs e)
    {
    }

    protected void Application_BeginRequest(object sender, EventArgs e)
    {        
    }

    protected void Application_EndRequest(Object sender, EventArgs e)
    {
    }

    public override string GetVaryByCustomString(HttpContext context, string custom)
    {
    }


    protected void Application_Error(object sender, EventArgs e)
    {
    }

    protected void Application_End(object sender, EventArgs e)
    {
    }

}

Notice that the global implementation inherits from HttpApplication which is the top level exposed ASP.NET component that manages pipeline of events and its processing (among other things). There are actually multiple instances of HttpApplication active at any given time depending on the load on the application and each instance processes requests on its own separate thread. If you’re interested in how the pipeline works and how requests and HttpApplicaiton objects map you can take a look at an oldish article of mine A low-level Look at the ASP.NET Architecture which explains this and a few other low level topics. It’s based on IIS 5/6 so there are some changes in the way the low level hooks happen in IIS 7, but the pipeline related topics still apply. Logically the ASP.NET pipeline hasn’t changed drastically with IIS 7 although physically there are siginificant processing changes.

HttpApplication is also responsible for hooking up and executing HttpHandlers and HttpModules which typically is done declaratively in web.config. Modules can also be added dynamically in code very early in the pipeline process. I don’t think HttpHandlers can be added in code. ASP.NET internally uses some very complex logic to find modules and handlers from various locations and hooks them up to new HttpApplication instances and finally manages the execution via StepManager.

Application Events are HttpModule ShortCuts

The Application_ events that you see in global.asax are effectively shortcuts for HttpModules as they map the events in the HttpApplication object. The Application_ level events are easier to deal with than a module, since you can simply implement them without having to register a module in web.config. For relatively simple application level logic that doesn’t need to be reused elsewhere this is perfect. For example, in my apps I tend to put generic logging and error handler code here. Full HttpModules are useful for more complex operations that require isolation of code or for anything that needs to be reused in other ASP.NET applications.

Ok, so much for HttpApplication 101. So what about those auto Application_ methods, how the heck do they get hooked up? So we know that Application_ handlers are hooked to HttpApplication events. It’s also clear that the bindings are not static, but rather have to be figured out by the compiler at runtime. Static binding is definitely not the way this is done because you can easily add ANY HttpApplication event handler using the Application_EventName syntax. So if you want to handle the HttpApplication::PreRequestHandlerExecute event you can simple add:

protected void Application_PreRequestHandlerExecute(object sender, EventArgs e)
{
    LogManager.Current.WriteEntry(new WebLogEntry()
    {
        ErrorLevel = ErrorLevels.Message,
        Message = "PreRequestHandlerExecute"
    });
}

and the code gets automagically fired at the appropriate time in the ASP.NET pipeline event processing. You can effectively implement any HttpApplication event this way in global.asax. Application_Start/_End/_EndSession are specical cases. The Start and Stop methods are manually configured by ASP.NET because there are no matching events (they basically get fired off Init and Dispose) and the Session related events require a special event signature. They are forwarded as Application_ events because they are obviously quite useful and also correspond to Classic ASP events that were available.

Clearly there’s a lot more going on behind the scenes than just statically binding events when Application_ events are bound. The ASP.NET parser has to actively parse global class figure out which events are being referenced with this specialized magic string syntax and bind the events for you.  So how does this happen? ASP.NET is definitely using Reflection on the global class to determine available Application_ methods and matching them to HttpApplication events. The answer to this took a bit of spelunking with Red Gate’s Reflector and the help of a few folks on Twitter (particularily Scott Allen, Peter Bromberg and  Bryan Cooke).

The key behavior is located in HttpApplicationFactor.ReflectOnApplicationType and specifically ReflectOnMethodInfoIfItLooksLikeEventHandler (say that a couple of times in a row :-}). The HttpApplicationFactory – as the name suggests – is responsible for feeding the HttpRuntime instance new instances of HttpApplication objects. It effectively instantiates the new instance and gets it ready for processing. The ReflectOnApplicationType() method is called from the HttpContext.CompileApplication() which in turn is part of the initial instance retrieval process of HttpContextFactory. If you look at the call tree in Reflector (using the Analyze feature) you can trace back the call sequence all the way to HttpRuntime.ProcessRequest:

ReflectorReflectOnApplicationType

Inside of ReflectOnApplicationType loops through all instance methods of your global class and calls ReflectOnMethodInfoIfItLooksLikeEventHandler to check and see if the method has the right signature (Event Handler signature basically). If it does the method is added to the list of event handlers:

private void ReflectOnApplicationType()
{
    ArrayList list = new ArrayList();
    foreach (MethodInfo info in this._theApplicationType.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | 
BindingFlags.Static | BindingFlags.Instance)) { if (this.ReflectOnMethodInfoIfItLooksLikeEventHandler(info)) { list.Add(info); } } Type baseType = this._theApplicationType.BaseType; if ((baseType != null) && (baseType != typeof(HttpApplication))) { foreach (MethodInfo info2 in baseType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance)) { if (info2.IsPrivate && this.ReflectOnMethodInfoIfItLooksLikeEventHandler(info2)) { list.Add(info2); } } } this._eventHandlerMethods = new MethodInfo[list.Count]; for (int i = 0; i < this._eventHandlerMethods.Length; i++) { this._eventHandlerMethods[i] = (MethodInfo)list[i]; } } private bool ReflectOnMethodInfoIfItLooksLikeEventHandler(MethodInfo m) { ParameterInfo[] parameters; string str; if (m.ReturnType == typeof(void)) { parameters = m.GetParameters(); switch (parameters.Length) { case 0: goto Label_007A; case 2: if (parameters[0].ParameterType == typeof(object)) { if ((parameters[1].ParameterType != typeof(EventArgs)) && !parameters[1].ParameterType.IsSubclassOf(typeof(EventArgs))) { return false; } goto Label_007A; } return false; } } return false; Label_007A: str = m.Name; int index = str.IndexOf('_'); if ((index <= 0) || (index > (str.Length - 1))) { return false; } if (StringUtil.EqualsIgnoreCase(str, "Application_OnStart") || StringUtil.EqualsIgnoreCase(str, "Application_Start")) { this._onStartMethod = m; this._onStartParamCount = parameters.Length; } else if (StringUtil.EqualsIgnoreCase(str, "Application_OnEnd") || StringUtil.EqualsIgnoreCase(str, "Application_End")) { this._onEndMethod = m; this._onEndParamCount = parameters.Length; } else if (StringUtil.EqualsIgnoreCase(str, "Session_OnEnd") || StringUtil.EqualsIgnoreCase(str, "Session_End")) { this._sessionOnEndMethod = m; this._sessionOnEndParamCount = parameters.Length; } return true; }

ah you gotta love the goto statements in this code. Surprising how often that crops up in the ASP.NET runtime code. You can see the Reflection code retrieving anything that contains in underscore in the name. Notice also that several events like Application_Start/End Session_End are special cased since there are no specific matching events.

This code is actually callled by HttpApplicationFactory.CompileApplication():

private void CompileApplication()
{
    this._theApplicationType = BuildManager.GetGlobalAsaxType();
    BuildResultCompiledGlobalAsaxType globalAsaxBuildResult = BuildManager.GetGlobalAsaxBuildResult();
    if (globalAsaxBuildResult != null)
    {
        if (globalAsaxBuildResult.HasAppOrSessionObjects)
        {
            this.GetAppStateByParsingGlobalAsax();
        }
        this._fileDependencies = globalAsaxBuildResult.VirtualPathDependencies;
    }
    if (this._state == null)
    {
        this._state = new HttpApplicationState();
    }
    this.ReflectOnApplicationType();
}

The actual hookup of events occurs in HttpApplication.HookupEventHandlersForApplicationAndModules which is called during HttpApplication Initialization and there’s some special processing in that method that checks for the “Application” prefix and if so points the handler at itself via this and hooks the event processing appropriately.

private void HookupEventHandlersForApplicationAndModules(MethodInfo[] handlers)
{
    this._currentModuleCollectionKey = "global.asax";
    if (this._pipelineEventMasks == null)
    {
        Dictionary<string, RequestNotification> eventMask = new Dictionary<string, RequestNotification>();
        this.BuildEventMaskDictionary(eventMask);
        if (this._pipelineEventMasks == null)
        {
            this._pipelineEventMasks = eventMask;
        }
    }
    for (int i = 0; i < handlers.Length; i++)
    {
        MethodInfo arglessMethod = handlers[i];
        string name = arglessMethod.Name;
        int index = name.IndexOf('_');
        string str2 = name.Substring(0, index);
        object obj2 = null;
        if (StringUtil.EqualsIgnoreCase(str2, "Application"))
        {
            obj2 = this;
        }
        else if (this._moduleCollection != null)
        {
            obj2 = this._moduleCollection[str2];
        }
        if (obj2 != null)
        {
            Type componentType = obj2.GetType();
            EventDescriptorCollection events = TypeDescriptor.GetEvents(componentType);
            string str3 = name.Substring(index + 1);
            EventDescriptor descriptor = events.Find(str3, true);
            if ((descriptor == null) && StringUtil.EqualsIgnoreCase(str3.Substring(0, 2), "on"))
            {
                str3 = str3.Substring(2);
                descriptor = events.Find(str3, true);
            }
            MethodInfo addMethod = null;
            if (descriptor != null)
            {
                EventInfo info3 = componentType.GetEvent(descriptor.Name);
                if (info3 != null)
                {
                    addMethod = info3.GetAddMethod();
                }
            }
            if (addMethod != null)
            {
                ParameterInfo[] parameters = addMethod.GetParameters();
                if (parameters.Length == 1)
                {
                    Delegate handler = null;
                    if (arglessMethod.GetParameters().Length == 0)
                    {
                        if (parameters[0].ParameterType != typeof(EventHandler))
                        {
                            goto Label_01F4;
                        }
                        ArglessEventHandlerProxy proxy = new ArglessEventHandlerProxy(this, arglessMethod);
                        handler = proxy.Handler;
                    }
                    else
                    {
                        try
                        {
                            handler = Delegate.CreateDelegate(parameters[0].ParameterType, this, name);
                        }
                        catch
                        {
                            goto Label_01F4;
                        }
                    }
                    try
                    {
                        addMethod.Invoke(obj2, new object[] { handler });
                    }
                    catch
                    {
                        if (HttpRuntime.UseIntegratedPipeline)
                        {
                            throw;
                        }
                    }
                    if ((str3 != null) && this._pipelineEventMasks.ContainsKey(str3))
                    {
                        if (!StringUtil.StringStartsWith(str3, "Post"))
                        {
                            this._appRequestNotifications |= this._pipelineEventMasks[str3];
                        }
                        else
                        {
                            this._appPostNotifications |= this._pipelineEventMasks[str3];
                        }
                    }
                Label_01F4: ;
                }
            }
        }
    }
}

This code is pretty cryptic – basically it’s all based on naming conventions with the name of methods matching up to the name of the event they are binding to. If a match is found the event is bound. If you follow some the intermediate methods of this code through in Reflector (or in the Symbol source if you’re more adventurous) it makes your head spin, as the code goes through some wicked gyrations to hook up the event handlers and then handle execution.

No need to know, but good to know

This is one of those things that is not really necessary to understand – knowing how this works is not likely to ever be an issue in your day to day development tasks. But as I got that message today I was intrigued enough by the realization that I didn’t know how this works although at first blush I thought I did. Then there’s this nagging feeling, well, you know how that goes… anyway, I’m doing a low level ASP.NET session in November at ASP.NET Connections and it turns out this was a good way to jog my memory about the low level gymnastics that ASP.NET goes through in request pipeline setup. So, not a complete waste of time of an hour or so, eh? :-}

Posted in ASP.NET  

The Voices of Reason


 

Wyatt Barnett
June 18, 2009

# re: How do ASP.NET Application_ Events Work

Awesome article, as always. I still reference the low level application article from time to time--it is even more pertinent in the days of IIS7's integrated pipeline. You do note "I don’t think HttpHandlers can be added in code. ", which is true, but you can get around that by using a IHttpHandlerFactory (http://msdn.microsoft.com/en-us/library/system.web.ihttphandlerfactory.aspx). It was System.Web.Routing before there was System.Web.Routing . . .

PS: looks like there is a little encoding error, I am seeing "453 of <%= App.Configuration.CommentMaxLength %> characters" below the comment preview.

Rick Strahl
June 19, 2009

# re: How do ASP.NET Application_ Events Work

@Wyatt - yeah handler factories do work but only if you instantiate the handler yourself in the actual ProcessRequest call that executes the handler. What I was getting at is adding a handler to the list of supported handlers and extensions dynamically at runtime. You can load modules dynamically in the initialization of the HttpApplication instance, but AFAIK you can't do that with handlers.

HandlerFactories still need to be hooked up in Web.config OR you can instantiate a handler and fire it yourself. But you can't load up a handler to the list in the same way you add them to web.config (ie. create a handler on the fly that maps say "MyHandle.axd" and then routes to my HttpHandler instance).

Funny you mention the character count here - I JUST fixed and was testing as I was reading your message. Classic old JavaScript code moved from an ASPX page into a separate script file that bit me on that one.

Doogal
June 19, 2009

# re: How do ASP.NET Application_ Events Work

I'm not sure that those gotos are real, I think it's Reflector getting confused. I will have to have a look at the .NET source (although that has stopped working on my machine for some reason)

Nicholas Piasecki
June 19, 2009

# Nifty

Neat! I've always kind of wondered about this, but never sat down long enough to figure it out. (I remember one day I was writing an event handler as usual, and then it dawned on me--Hey! There's no "override" there! How does that work?!)

Matthew Noonan
June 19, 2009

# re: How do ASP.NET Application_ Events Work

@Doogal - The GOTOs are not a mistake in Reflector! I have had this conversation multiple times. Somebody once asked me if GOTOs were still valid in VB.NET, and I responded: not just VB.NET, you can use them in C# as well. Of course, the next question was: Why?

Without getting too geeky, my response is usually something like: how can you get rid of a feature which is built into the CPU? Anyone who is familiar with assembler can tell you about the JMP instruction, which is basically the same as a GOTO.

Now back to Reflector. A common misconception is that Reflector is displaying the code as it was typed by the programmer. This is not true, Reflector displays the code as it is reverse engineered from the MSIL. The MSIL was, of course, created by the compiler, which will frequently compile and optimize your code using, you guessed it, GOTO!

Rick Strahl
June 19, 2009

# re: How do ASP.NET Application_ Events Work

@Matthew - I think the only places I've seen GOTO's though is in Microsoft code. Never seen (or noticed it) in other Reflector reviewed code.

It seems it's always code to break out of a loop. GOTO's maybe very ugly from a maintenance POV, but they are often the most efficient way to handle the most common path through code. I still find it amusing when I see it in code even if decompiled. :-}

Roberto Hernandez
June 19, 2009

# re: How do ASP.NET Application_ Events Work

@Rick

I have seen them in my code using Reflector and I have never used a GOTO in either C# or VB.NET. They are probably compiler optimizations. Having said that, they do represent a code smell of sorts as they only crop on methods which have a High Cyclomatic Complexity (CC).

Steve from Pleasant Hill
June 19, 2009

# re: How do ASP.NET Application_ Events Work

Assembly language JMPs...

Peter Bromberg
June 21, 2009

# re: How do ASP.NET Application_ Events Work

Nice to see you went "all the way" on this, Rick. Well done, and quite revealing!

Matthew Noonan
June 22, 2009

# re: How do ASP.NET Application_ Events Work

@Rick - Perhaps MS has some compiler switches turned on that are not the defaults? I have seen the GOTOs in other non-MS products, but I agree they don't seem to appear in my disassemblies that often. Still, I would laugh if I actually saw an MS engineer put a GOTO in the middle of their C# code. It may be efficient, but I can't believe it is encouraged! :)

Gerardo Contijoch
June 27, 2009

# re: How do ASP.NET Application_ Events Work

Great post! I love to know this kind of stuff.

Milan Negovan
June 28, 2009

# re: How do ASP.NET Application_ Events Work

Rick,

In a similar vein, I beat my head against the wall with all those Session_Xxxx automagical events: http://www.aspnetresources.com/articles/event_handlers_in_global_asax.aspx

Hope this helps,
Milan

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024