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:
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? :-}
Other Posts you might also like