The Global.asax, also known as the ASP.NET application file, is used to serve application-level and session-level events. Although its structure is pretty simple and it’s easy to have one in the project, many developers don’t know exactly how it works. This post aims at revealing some of the mystery behind this file as well as provides you with some good usage examples.
Let’s first have a look at a sample Global.asax file:
<%@ Application Language="C#" %>
<%@ Import Namespace="System.Diagnostics" %>
<script runat="server">
public Guid AppId = Guid.NewGuid();
public String TestMessage;
public String GetAppDescription(String eventName)
{
StringBuilder builder = new StringBuilder();
builder.AppendFormat("-------------------------------------------{0}",
Environment.NewLine);
builder.AppendFormat("Event: {0}{1}", eventName, Environment.NewLine);
builder.AppendFormat("Guid: {0}{1}", AppId, Environment.NewLine);
builder.AppendFormat("Thread Id: {0}{1}",
System.Threading.Thread.CurrentThread.ManagedThreadId, Environment.NewLine);
builder.AppendFormat("Appdomain: {0}{1}",
AppDomain.CurrentDomain.FriendlyName, Environment.NewLine);
builder.AppendFormat("TestMessage: {0}{1}", TestMessage, Environment.NewLine);
builder.Append((System.Threading.Thread.CurrentThread.IsThreadPoolThread ?
"Pool Thread" : "No Thread") + Environment.NewLine);
return builder.ToString();
}
void Application_Start(object sender, EventArgs e)
{
TestMessage = "not null";
Trace.WriteLine(GetAppDescription("Application_Start"));
}
void Application_End(object sender, EventArgs e)
{
Trace.WriteLine(GetAppDescription("Application_End"));
}
void Application_Error(object sender, EventArgs e)
{
Trace.WriteLine(GetAppDescription("Application_Error"));
}
void Application_BeginRequest(object sender, EventArgs e)
{
Trace.WriteLine(GetAppDescription("Application_BeginRequest"));
}
void Application_EndRequest(object sender, EventArgs e)
{
Trace.WriteLine(GetAppDescription("Application_EndRequest"));
}
</script>
Global.asax is compiled to its own assembly – if you publish the web project from Visual Studio, you can find App_global.asax.dll in the bin directory, otherwise it will go to c:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\<appname>. After decompiling the produced assembly, we will receive something like (methods are collapsed for clarity):
namespace ASP
{
[CompilerGlobalScope]
public class global_asax : HttpApplication
{
private static bool __initialized;
public Guid AppId = Guid.NewGuid();
public String TestMessage;
protected DefaultProfile Profile
{
get
{
return (DefaultProfile)base.Context.Profile;
}
}
[DebuggerNonUserCode]
public global_asax()
{
if (!global_asax.__initialized)
{
global_asax.__initialized = true;
}
}
}
}
From the above code, we can see that a new class (global_asax
) was created in the ASP namespace which inherits from HttpApplication
. HttpApplication
is an interesting class itself. It implements the ASP.NET request pipeline, invoking HttpModules
, page event handlers, etc. A quite common mistake is to think of the HttpApplication
(global_asax
) as a singleton shared by all the requests. The truth is that each request has its own instance of the HttpApplication
class. To be more specific, the ASP.NET runtime keeps two pools of HttpApplication
objects. The first is a special pool with HttpApplication
objects used for application level events (such as Application_Start
, Application_End
, and Application_Error
), the second pool contains instances used in requests to serve all other types of events. Which class is then responsible for managing those pools? Here is where the HttpApplicationFactory
comes into play. There is only one instance of this class per HttpRuntime
and besides managing app pools, it is also responsible for compiling Global.asax and ensuring that the Application_Start
/End
events are called only once.
To prove what I described, let’s use the above Global.asax file in a very simple web application. We will use System.Diagnostics.Trace
so some changes to the web.config file are required:
="1.0"
<configuration>
<system.diagnostics>
<trace autoflush="true">
<listeners>
<add name="LogFile"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="webapp.log"/>
</listeners>
</trace>
</system.diagnostics>
<system.codedom>
<compilers>
<compiler language="c#" extension=".cs" compilerOptions="/d:TRACE"
type="Microsoft.CSharp.CSharpCodeProvider, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"
warningLevel="1"/>
</compilers>
</system.codedom>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
</configuration>
After the first client request, your webapp.log file should contain something similar to:
-------------------------------------------
Event: Application_Start
Guid: d8ab181d-b1f0-4733-83c1-04e6af5e6038
Thread Id: 1
Appdomain: /LM/W3SVC/3/ROOT-1-129556455331183204
TestMessage: not null
No Thread
-------------------------------------------
Event: Application_BeginRequest
Guid: 22089cce-f79a-4123-b455-e7f9e1091dd5
Thread Id: 5
Appdomain: /LM/W3SVC/3/ROOT-1-129556455331183204
TestMessage:
Pool Thread
-------------------------------------------
Event: Page_Load
Guid: 22089cce-f79a-4123-b455-e7f9e1091dd5
Thread Id: 5
Appdomain: /LM/W3SVC/3/ROOT-1-129556455331183204
TestMessage:
Pool Thread
-------------------------------------------
Event: Application_EndRequest
Guid: 22089cce-f79a-4123-b455-e7f9e1091dd5
Thread Id: 5
Appdomain: /LM/W3SVC/3/ROOT-1-129556455331183204
TestMessage:
Pool Thread
The different GUIDs for Application_Start
and Application_…Request
events prove that there were two global_asax
instances involved in processing the request. The first one is the special one and will be stored in the special pool (probably reused for Application_End
/Application_Error
events), the second one is a normal instance that will be reused in further requests. Now look at the TestMessage
property value – it was set only for the HttpApplication
instance that was created as the first one. That assures us that the Application_Start
is called only once in the web application lifetime (and not for every newly created HttpApplication
instance).
So what’s the correct way of using global.asax? First, remember:
- It’s not a singleton.
Application_Start
/End
events are called only once. - There is one instance of
global_asax
per request and additionally there are instances for special events.
Second, try to apply those guidelines to your Global.asax files:
- All
private
fields should be initialized in the constructor. Static
fields may be initialized either in the Application_Start
or a static
constructor. - Don’t use
lock
blocks – your request possesses this instance of the global_asax
.
You can stop reading here if what I wrote is satisfactory for you. If you still have some doubts and would like to dig deeper, keep on reading :) – I will show you how to look at the ASP.NET internals with the SOS debugger extension. Basically, you start with attaching to the w3wp.exe process that your application is running in. Then load sos (replace clr with mscorwks if it’s .NET 2.0):
.loadby sos clr
Then, let’s find the HttpApplicationFactory
instance (highlighted):
0:024> !DumpHeap -type System.Web.HttpApplicationFactory
------------------------------
Heap 0
Address MT Size
total 0 objects
------------------------------
Heap 1
Address MT Size
000000013ff40e78 000007fee65c1900 136
total 0 objects
------------------------------
total 0 objects
Statistics:
MT Count TotalSize Class Name
000007fee65c1900 1 136 System.Web.HttpApplicationFactory
Total 1 objects
Then dump its content – I highlighted the special and normal application pools (which are actually of type System.Collections.Stack
):
0:024> !do 000000013ff40e78
Name: System.Web.HttpApplicationFactory
MethodTable: 000007fee65c1900
EEClass: 000007fee63147a8
Size: 136(0x88) bytes
File: C:\Windows\Microsoft.Net\assembly\GAC_64\System.Web\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Web.dll
Fields:
MT Field Offset Type VT Attr Value Name
000007fee8b1d6e8 4000bf3 78 System.Boolean 1 instance 1 _inited
000007fee8b169d0 4000bf4 8 System.String 0 instance 000000013ff41070 _appFilename
000007fee8b1f8a0 4000bf5 10 ...tions.ICollection 0 instance 0000000000000000 _fileDependencies
000007fee8b1d6e8 4000bf6 79 System.Boolean 1 instance 1 _appOnStartCalled
000007fee8b1d6e8 4000bf7 7a System.Boolean 1 instance 0 _appOnEndCalled
000007fee65c1988 4000bf8 18 ...pApplicationState 0 instance 000000013ff410c8 _state
000007fee8b18358 4000bf9 20 System.Type 0 instance 000000013ff40e48 _theApplicationType
000007fee8b2d360 4000bfa 28 ...Collections.Stack 0 instance 000000013ff40f00 _freeList
000007fee8b1c8b8 4000bfb 60 System.Int32 1 instance 1 _numFreeAppInstances
000007fee8b1c8b8 4000bfc 64 System.Int32 1 instance 1 _minFreeAppInstances
000007fee8b2d360 4000bfd 30 ...Collections.Stack 0 instance 000000013ff40f98 _specialFreeList
000007fee8b1c8b8 4000bfe 68 System.Int32 1 instance 1 _numFreeSpecialAppInstances
... continued ...
To find out which HttpApplication
instances are stored in the particular pool, you need to dump the pool and then its internal array content:
0:024> !do 000000013ff40f00
Name: System.Collections.Stack
MethodTable: 000007fee8b2d360
EEClass: 000007fee87561c0
Size: 40(0x28) bytes
File: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
000007fee8b1aed8 4000b9f 8 System.Object[] 0 instance 000000013ff40f28 _array
000007fee8b1c8b8 4000ba0 18 System.Int32 1 instance 1 _size
000007fee8b1c8b8 4000ba1 1c System.Int32 1 instance 1 _version
000007fee8b15b28 4000ba2 10 System.Object 0 instance 0000000000000000 _syncRoot
0:024> !da 000000013ff40f28
Name: System.Object[]
MethodTable: 000007fee8b1aed8
EEClass: 000007fee869fdb8
Size: 112(0x70) bytes
Array: Rank 1, Number of elements 10, Type CLASS
Element Methodtable: 000007fee8b15b28
[0] 000000013ff76bc8
[1] null
[2] null
[3] null
[4] null
[5] null
[6] null
[7] null
[8] null
[9] null
To check that this instance is the one that we got in the log file, we need to look through its properties and compare, for example, the GUID value:
0:024> !do 000000013ff76bc8
Name: ASP.global_asax
MethodTable: 000007ff00147a80
EEClass: 000007ff001b25f0
Size: 240(0xf0) bytes
File: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\
Temporary ASP.NET Files\root\8fbb8387\1608fccf\
assembly\dl3\7d155608\c7473357_d746cc01\App_global.asax.dll
Fields:
MT Field Offset Type VT Attr Value Name
000007fee65c1988 4000b84 8 ...pApplicationState 0 instance 000000013ff410c8 _state
... some fields here ...
000007fee8b38738 4000002 d8 System.Guid 1 instance 000000013ff76ca0 AppId
000007fee8b169d0 4000003 d0 System.String 0 instance 0000000000000000 TestMessage
000007fee8b1d6e8 4000001 40 System.Boolean 1 static 1 __initialized
0:024> dd 000000013ff76ca0
00000001`3ff76ca0 22089cce 4123f79a f9e755b4 d51d09e1
// which is 22089cce-f79a-4123-b455-e7f9e1091dd5
00000001`3ff76cb0 00000000 00000000 e8b21c98 000007fe
00000001`3ff76cc0 3ff76d10 00000001 00000000 00000000
00000001`3ff76cd0 00000000 00000000 00000000 00000000
00000001`3ff76ce0 00000000 00000000 00000000 00000000
00000001`3ff76cf0 00000001 00000000 00000002 3f3851ec
00000001`3ff76d00 00000001 00000000 00000000 00000000
00000001`3ff76d10 e8b21f18 000007fe 00000003 00000000
You may perform the same steps to examine the special application pool.
I hope that this post helped you better understand the Global.asax file and its role in ASP.NET applications. A simple web project is available for download here.