Cutting Edge
The Server Side of ASP.NET Pages
Dino Esposito
Code download available at: Cutting Edge 2007_01.exe(276 KB)
Contents
What's in the Temporary ASP.NET Folder?
Preservation Files
Page Class Dynamic Source Code
The Runtime Public API
Building the Explorer Tool
Examining Sample Pages
Wrap-Up
As an ASP.NET developer, you probably have a pretty good idea about how ASP.NET handles code in an .aspx resource, how that markup is parsed and transformed into a Visual Basic or C# class on the fly, and so forth. But what happens next? Where are the files generated by ASP.NET stored and how are they used to serve page requests? Last month, I began my look at this process. This month, I'll examine what happens on the server so you can avoid some common pitfalls. I'll review the storage of temporary ASP.NET files and the source code of dynamically generated classes used to serve page responses. In addition, I'll build an explorer tool you can use with any ASP.NET 2.0 or ASP.NET AJAX (formerly code-named "Atlas") application to look into and debug the actual code your pages execute. But before we do that, there are a few things you'll need to know. (As with last month's column, this installment is largely based on undocumented details of how ASP.NET works. These implementation details may change in future versions of the Microsoft .NET Framework.)
What's in the Temporary ASP.NET Folder?
The processing of an ASP.NET page request requires quite a few temporary files. When you install ASP.NET 2.0 on a Web server machine, a hierarchy of folders is created like so:
%WINDOWS%\Microsoft.NET\Framework\v2.0.50727
The version number here refers to the retail version of ASP.NET 2.0. Each released version of ASP.NET, including each intermediate build, has a unique version number and creates a different tree of folders in order to support side-by-side execution. Therefore, it is extremely important that you specify which version of ASP.NET your app is designed for. Apps running under ASP.NET 1.x and ASP.NET 2.0 are based on physically separate folders. Under the Microsoft.NET\Framework folder you find as many vX.X.XXXX subfolders as there are installed versions of ASP.NET (see Figure 1).
Figure 1** ASP.NET 1.0, 1.1, 2.0, and 3.0 Runtime Files **(Click the image for a larger view)
Under the root folder of the installed version, you'll find a number of child directories. The CONFIG folder contains machine configuration files, including machine.config and the base web.config used for all sites. The folder named ASP.NETWebAdminFiles contains the source files that make up the Web Site Administration tool that you can run from within Visual Studio® 2005. Finally, the Temporary ASP.NET Files folder contains all temporary files and assemblies being created to serve pages and resources. You have to look into this subtree to find dynamically created files for your Web pages. Note that the Temporary ASP.NET Files directory is the default location for dynamically created files, but this location is configurable on a per-application basis using the <compilation> section in the web.config file:
<compilation tempDirectory="d:\MyTempFiles" />
The first time an application is executed on the machine, a new subfolder is created under the temporary files directory. The name of the compilation subfolder matches the name of the application's IIS virtual directory. If you're simply testing the application using the Visual Studio 2005 embedded Web server, then the subfolder takes the name of the root folder of the Web application. If you invoke pages from the Web server's root folder, you'll find their temp files under the root subfolder (see Figure 2).
Figure 2** WebApps on a Test Web Server **(Click the image for a larger view)
Under the application's compilation subfolder, there's a collection of directories based on hashed names. A typical path where you can find temp files is shown here. (The last two directories have fake but realistic names.)
\v2.0.50727\Temporary ASP.NET Files\MyWebApp\3678b103\e60405c7
You can programmatically retrieve the path to the subfolder where the temp files of a given application are located using the following statement:
Dim tempFilesFolder As String = HttpRuntime.CodegenDir
ASP.NET scavenges the compilation folders and removes stale resources periodically when an application is altered and a recompilation is required, however the size of the subtree rooted in Temporary ASP.NET Files may grow significantly, especially on test machines. As an administrator, you might want to keep an eye on the directories under Temporary ASP.NET Files and make sure that all of them refer to currently active applications. If you accidentally delete the subtree of an active application, don't fret. You'll lose all precompiled pages and resources and reset the application to its original compilation state; however, the next request will trigger a new compilation for each page or for a batch of pages, as configured, so in the end there will be no loss of information or pages (though users will experience that first-hit delay for the next requests). Let's now look at the contents of the compilation folder for a given application.
Preservation Files
For each page in the application, the page compilation process generates a file with the following name:
[page].aspx.[folder-hash].compiled
The [page] placeholder represents the name of the .aspx resource. The [folder-hash] placeholder is a hash value that makes the file name unique and disambiguates files with the same name that originally belonged to different folders. Such files are said to be preservation files because they contain important information that helps the ASP.NET runtime to quickly retrieve the assembly and type name of the HTTP handler that will be used to serve the page request. In addition, a preservation file contains a file hash value that is used to detect if the contents of the file has changed since last access.
All .aspx pages that make up an application are compiled in the same temp folder, even if they have the same name and reside in different folders. How does that work? Suppose your application contains two pages named test.aspx that are located in different folders-Folder1 and Folder2. Both pages will be compiled in the same temp folder, but they can be distinguished by their hash values, which will be different because hash values are calculated on the path information and not just the file name. So in the end, the two test.aspx pages might have preservation file names that differ only in the folder hash value:
Test.aspx.cdcab7d2.compiled
Test.aspx.9d86a5d7.compiled
An internally stored cache of hash values allows the ASP.NET runtime to identify hash values for any given page URL and quickly locate the corresponding preservation file. If no preservation file is found, ASP.NET compiles the page on the fly. This is exactly what happens when you deploy the application without precompilation. When you precompile a site, on the other hand, preservation files for each constituent page are created and placed in the Bin folder.
Preservation files are plain XML files. Figure 3 shows the contents of a sample preservation file.
Figure 3 Sample Preservation File
<?xml version="1.0" encoding="utf-8"?>
<preserve resultType="3" virtualPath="/WebSite2/Default.aspx"
hash="bce9c4d05"
filehash="2831dc3af5add65c"
flags="110000"
assembly="App_Web_ap-sequk"
type="ASP.default_aspx">
<filedeps>
<filedep name="/WebSite2/Default.aspx" />
<filedep name="/WebSite2/Default.aspx.vb" />
</filedeps>
</preserve>
Figure 4 details the attributes of the file. The <fileDeps> section lists the files the current page is dependent on. Any changes to any dependency will cause page recompilation. The FileHash value represents a snapshot of the state of dependencies, while Hash represents a snapshot of the state of the current page file. It's worth noting that a mechanism to detect on-the-fly changes to files that is based entirely on file change notifications will fail the moment you stop or restart the Web application. Persisting the state of pages and dependencies as hash values lets you detect changes at any time.
Figure 4 Attributes in the Preservation File
Attribute | Description |
---|---|
Assembly | Name of the assembly that contains the compiled page. |
FileHash | Hash value used to check for changes in all source files the page depends upon. |
Flags | Integer value used to persist the state of internal operations, such as building a hash value to monitor changes in dependent files. |
Hash | Hash value used to check the current page for any changes. |
ResultType | Indicates the type of build result for which this preservation file has been created. Feasible values come from a non-public enumeration named BuildResultTypeCode. |
Type | Name of the type that represents the compiled page. |
VirtualPath | Virtual path of the page being compiled. |
The type attribute sets the name of the dynamically created class that will be used to serve the request. By default, the type name is ASP.[page]_aspx, where [page] stands for the name of the page file. Note, though, that you can change this name by setting the ClassName attribute in the @Page directive of your .aspx file. The root namespace won't change, so the type name could be ASP.[ClassName].
The assembly attribute indicates the name of the dynamically created assembly that contains the page class to serve the request. The name and contents of such an assembly depends on the settings in the <compilation> section of the web.config file.
By default, application pages are compiled in batch mode, meaning that ASP.NET attempts to stuff as many uncompiled pages as possible into a single assembly. The attributes maxBatchSize and maxBatchGeneratedFileSize let you limit the number of pages packaged in a single assembly and the overall size of the assembly. By default, you will have no more than 1,000 pages per batched compilation and no assembly larger than 1MB. In general, you don't want users to wait too long when a large number of pages are compiled the first time. At the same time, you don't want to load a huge assembly in memory to serve only a small page, or to start compilation for each and every page. The maxBatchSize and maxBatchGeneratedFileSize attributes help you find a good balance between first-hit delay and memory usage. If you opt for site precompilation (see Fritz Onion's Extreme ASP.NET column in the January 2006 issue) you don't have to worry about first-hit delay, but you should still think about optimal batching parameters to avoid overloading the Web server's memory.
When batching is on, the first 1,000 pages in the application (the actual number depends on maxBatchSize) are compiled to an assembly named App_Web_[random], where [random] is a random sequence of eight characters. If batching is turned off, each page originates its own assembly. The name of the assembly is the following:
App_Web_[page].aspx.[folder-hash].[random].dll
To turn off batching, add the following to your web.config file:
<compilation batch="false" />
If you explore the compilation folder of a sample application, you'll find a companion preservation file that includes CBMResult in the name, as well as a .ccu file with the same name, like this:
test.aspx.cdcab7d2.compiled
test.aspx.cdcab7d2_CBMResult.ccu
test.aspx.cdcab7d2_CBMResult.compiled
The first file in the list is the preservation file. What about the other two? CCU stands for Code Compile Unit and refers to the CodeDOM tree created to generate the source code for the dynamic page class. The CCU file is a binary file that contains the serialized version of the CodeDOM tree for the page. The CBMResult file is the preservation file used to check if the CCU is up to date, where it is located, and which files it is based upon.
CBMResult files are consumed by the modules that communicate with the ClientBuildManager class-for example, Visual Studio 2005 designers and IntelliSense. These modules query the structure of the page for statement completion. The CCU file maintains an up-to-date copy of the CodeDOM structure of the page ready to service these requests.
Page Class Dynamic Source Code
As mentioned, an .aspx resource is parsed to a Visual Basic or C# class. The class inherits from System.Web.UI.Page or, more likely, from a class that inherits from System.Web.UI.Page. In the most common scenario, in fact, the dynamic page class has the following prototype:
Namespace ASP
Public Class test_aspx
Inherits Test : Implements System.Web.IHttpHandler
...
End Class
End Namespace
In the example, the class Test is defined in the code file class of the page and includes any event handlers and helper routines you write in the companion class file for the page. As you may have already noticed while working with Visual Studio 2005, this code file class lacks definitions for members of the page. For each runat=server tag you find in the .aspx source file, you should expect a member of the corresponding type defined in the code file. The ASP.NET runtime system generates a partial Test class that contains all these members plus two additional properties-Profile and ApplicationInstance. Figure 5 shows the set of classes that contribute to serve the request for a given .aspx resource.
Figure 5 Runtime Classes Used to Serve .aspx Request
'Generated by ASP.NET to add member definitions
Partial Public Class Test
Implements System.Web.SessionState.IRequiresSessionState
Protected WithEvents TextBox1 As System.Web.UI.WebControls.TextBox
Protected WithEvents Button1 As System.Web.UI.WebControls.Button
Protected WithEvents Msg As System.Web.UI.WebControls.Label
Protected WithEvents form1 As System.Web.UI.HtmlControls.HtmlForm
Protected ReadOnly Property Profile() _
As System.Web.Profile.DefaultProfile
Get
Return CType(Me.Context.Profile, _
System.Web.Profile.DefaultProfile)
End Get
End Property
Protected ReadOnly Property ApplicationInstance() _
As System.Web.HttpApplication
Get
Return CType(Me.Context.ApplicationInstance, _
System.Web.HttpApplication)
End Get
End Property
End Class
'Your code file
Partial Class Test : Inherits System.Web.UI.Page
Protected Sub Button1_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Button1.Click
End Sub
End Class
'Created to build the expected markup for the .aspx resource
Namespace ASP
Public Class test_aspx
Inherits Global.Test
Implements System.Web.IHttpHandler
...
End Class
End Namespace
The classes in Figure 5 span two distinct source files. The first contains the partial class to complete the class in the code file and the actual page class derived from that to serve the request. The second file is a copy of the code file you created in the project. These files are named after the assembly name. The pattern is as follows: [assembly].X.vb. (or .cs if you're using C#). X is a progressive, 0-based index to uniquely name files.
If you look at the contents of the compilation folder for a sample test.aspx page, you'll find that a third file is created, such as the one in this example:
Namespace __ASP
Friend Class FastObjectFactory_app_web_test_aspx_cdcab7d2_xg83msu0
Private Sub New()
MyBase.New
End Sub
Shared Function Create_ASP_test_aspx() As Object
Return New ASP.test_aspx
End Function
End Class
End Namespace
The class name is the name of the page assembly prefixed by the string FastObjectFactory. The class features one shared function (static if written in C#) named Create_XXX where XXX is the name of the page class to instantiate. As the name suggests, this is a helper class that the ASP.NET runtime uses to speed up the creation of page instances-a very frequent operation. Creating such a class takes a very small amount of time compared to the time it takes to compile a page. On the other hand, a factory class is considerably faster than the practice of indirect object creation using Activator.CreateInstance.
The contents of the factory class changes depending on the batch compilation setting. In the default case, when batching is on, the factory class contains as many Create_XXX functions as there are batched pages. The name of the factory class matches the name of the batch assembly:
' Used to serve test.aspx
Shared Function Create_ASP_test_aspx() As Object
Return New ASP.test_aspx
End Function
' Used to serve default.aspx
Shared Function Create_ASP_default_aspx() As Object
Return New ASP.default_aspx
End Function
If batching is off, the factory class matches the name of the individual page assembly and contains just one shared function-the factory of the specific page. In this case, each page in the application will have its own factory class.
The Runtime Public API
Armed with the information discussed thus far, exploring the contents of the compilation folder may not be very difficult. Still, a tool to help you quickly locate the information you want would be handy. In a minute, I'll design an explorer tool to navigate the dynamically generated source code of an ASP.NET application, but first let's look at some of the runtime APIs in the .NET Framework 2.0. In particular, there are two classes you might want to know more about: HttpRuntime and ClientBuildManager.
The HttpRuntime class features a number of shared properties that return information about a variety of system paths including the Bin folder of the current application, the ASP.NET installation, the compilation folder, and the current AppDomain ID. You can also easily obtain the list of assemblies loaded in the current AppDomain using the following code:
Dim listOfAssemblies() As Assembly
listOfAssemblies = AppDomain.CurrentDomain.GetAssemblies()
This code is not specific to ASP.NET, but when invoked from within an ASP.NET application it returns the array of assemblies in the AppDomain, including all of those generated for your pages.
The ClientBuildManager class doesn't have many informative properties, except for CodeGenDir which returns the same information as the CodeGenDir property on HttpRuntime. It does, however, feature a number of methods for reading configuration information (such as supported browsers) and for precompiling the application. One of the methods on the class, Get, returns a list of application directories monitored for critical changes that will shut down the AppDomain. These directories are: App_Browsers, App_Code, App_GlobalResources, App_WebReferences and Bin.
Building the Explorer Tool
For debugging, it's often useful to have quick access to the source code of the running page along with other runtime information. Any tool that provides that functionality would have to be compatible with any ASP.NET application and require limited configuration or no configuration at all. Nikhil Kothari's excellent Web Development Helper tool would be perfect, if only it provided ASP.NET runtime information. The tool is implemented as a Browser Helper Object (BHO), a COM-based plug-in for the Microsoft Internet Explorer user interface. A BHO would have been a great host environment for the spy tool I built for this column but, unfortunately, I'm too lazy to go that route. So I wrote my tool as an HTTP module that sits between your page and the browser, looks up the query string, and kicks in if explicitly invoked. Installing the HTTP module in an ASP.NET application requires only one extra line in the web.config and can be turned on and off easily:
<httpModules>
<add name="AspExplorerModule" type="Samples.AspExplorerModule" />
</httpModules>
Figure 6 shows most of the code of the Explorer HTTP module. The module registers for the PostMapRequestHandler application event and hooks up the page class. The PostMapRequestHandler event fires when the ASP.NET runtime has determined the HTTP handler object needed to serve the request. If the request contains the source=true parameter on the query string, and the handler is a class that inherits from System.Web.UI.Page, the module starts working.
Figure 6 ASP Explorer HTTP Module
Namespace Samples
Public Class AspExplorerModule : Implements IHttpModule
Private _app As HttpApplication
Public Sub Dispose() Implements IHttpModule.Dispose
End Sub
Public Sub Init(ByVal context As HttpApplication) _
Implements IHttpModule.Init
If context Is Nothing Then
Throw New ArgumentNullException("[context] argument")
End If
' Cache the HTTP application reference
_app = context
' Map the app event to hook up the page
AddHandler context.PostMapRequestHandler, _
New EventHandler(AddressOf OnPostMapRequestHandler)
End Sub
Private Sub OnPostMapRequestHandler( _
ByVal source As Object, ByVal e As EventArgs)
' Get the just mapped HTTP handler and cast to Page
Dim theHandler As IHttpHandler = _app.Context.Handler
Dim thePage As Page = TryCast(theHandler, Page)
' If OK, register an event handler for PreRender
If thePage IsNot Nothing AndAlso IsSourceRequest() Then
HookUpPage(thePage)
End If
End Sub
Private Function IsSourceRequest() As Boolean
Return String.Equals( _
app.Request.QueryString.Item("source"), "true", _
StringComparison.OrdinalIgnoreCase)
End Function
Sub HookUpPage(ByVal thePage As Page)
AddHandler thePage.PreRenderComplete, _
New EventHandler(AddressOf PreRenderComplete)
End Sub
Private Sub PreRenderComplete( _
ByVal sender As Object, ByVal e As EventArgs)
Dim thePage As Page = DirectCast(sender, Page)
thePage.SetRenderMethodDelegate( _
New RenderMethod(AddressOf RenderSource))
End Sub
Private Sub RenderSource( _
ByVal output As HtmlTextWriter, _
ByVal container As Control)
Dim thePage As Page = DirectCast(container, Page)
Dim source As String
source = GetWebPageInfo(thePage)
output.Write(source)
End Sub
Private Function GetWebPageInfo(ByVal thePage As Page) As String
...
End Function
End Class
End Namespace
The ASP Explorer module hooks up the page class and registers its own handler for the PreRenderComplete event. Architected in this way, the HTTP module doesn't alter the runtime processing of the request and doesn't interfere with the compilation of the page either. When the source parameter is specified on the query string and set to true, the module kicks in. As you can see in Figure 6, all the module does is register a rendering delegate for the page using the little-known Page class method, SetRenderMethodDelegate. When a rendering delegate is specified for a page, the wrapped method overrides the standard rendering. In other words, once the module is installed, if you place a call to test.aspx, you'll see the standard output of the page; if you place a call to test.aspx?source=true, you'll see all the runtime information about the page the module can collect.
The ASP Explorer source code defines a class to map the contents of the preservation file for the current page. It reads the preservation file and in the class in Figure 7 copies any information. The SourceFiles property is a collection designed to contain all source files that contribute to the page. The collection is populated with information not found in the preservation file, but still obtained from the compilation folder. In particular, this includes all source files that relate to a given page that are .vb or .cs files whose name begins with the name of dynamic page assembly. The GetWebPageInfo method (see Figure 6) captures any information and builds the output for the request in source mode. The page output includes runtime information as well as the source code of dynamic page classes. Figure 8 shows ASP Explorer in action.
Figure 7 Helper Class to Load Information from Preservation Files
Public Class PreservationInfo
Private _file As String
Private node As XmlNode
Public Sub New(ByVal file As String)
_file = file
Load()
End Sub
Private Sub Load()
Dim doc As New XmlDocument
doc.Load(_file)
node = doc.DocumentElement
If node Is Nothing OrElse _
Not String.Equals(node.Name, "preserve") Then Return
Dim buf As String
' ResultType
buf = node.Attributes("resultType").Value
Int32.TryParse(buf, ResultType)
' VirtualPath
VirtualPath = node.Attributes("virtualPath").Value
' Hash
Hash = node.Attributes("hash").Value
' FileHash
FileHash = node.Attributes("filehash").Value
' Flags
buf = node.Attributes("flags").Value
Int32.TryParse(buf, Flags)
' Assembly
AssemblyName = node.Attributes("assembly").Value
' Type
TypeName = node.Attributes("type").Value
' Dependencies
Dim list As XmlNodeList = node.SelectNodes("filedeps/filedep")
FileDeps = Array.CreateInstance(GetType(String), list.Count)
For i As Integer = 0 To list.Count - 1
FileDeps(i) = list.Item(i).Attributes("name").Value
Next
' Source files
Dim searchPath As String = String.Format("{0}.*", AssemblyName)
Dim files() As String = _
Directory.GetFiles(HttpRuntime.CodegenDir, searchPath)
SourceFiles = Array.CreateInstance(GetType(String), files.Length)
For i As Integer = 0 To files.Length - 1
Dim fileInfo As New FileInfo(files(i))
If fileInfo.Extension = ".vb" Or _
fileInfo.Extension = ".cs" Then
SourceFiles(i) = files(i)
End If
Next
End Sub
Public ResultType As Integer
Public VirtualPath As String
Public Hash As String
Public FileHash As String
Public Flags As Long
Public AssemblyName As String
Public TypeName As String
Public FileDeps() As String
Public SourceFiles() As String
End Class
Figure 8** ASP Explorer Module in Action **(Click the image for a larger view)
Examining Sample Pages
Now that you have the tool, let's take a brief look at the structure of the code that ASP.NET generates for each .aspx file. It is interesting to note that without the parsing and compilation facilities provided by the ASP.NET runtime, you would have to write that code yourself in order to run an ASP.NET page!
The dynamic page class (the test_aspx class in Figure 5) overrides a few methods on the System.Web.UI.Page class: FrameworkInitialize, ProcessRequest, and GetTypeHashCode. Nothing special happens in ProcessRequest, which simply calls its base method. GetTypeHashCode returns the hash code for the page that uniquely identifies the page's control hierarchy. The hash value is calculated on the fly when the page is compiled and is inserted as a constant in the source.
The most interesting override is FrameworkInitialize. The method governs the creation of the page's control tree and calls into a private method named __BuildControlTree. This method populates the Controls collection of the page class with fresh instances of the controls corresponding to runat=server tags in the .aspx source. __BuildControlTree parses all server-side tags and builds an object for each of them.
<asp:textbox runat="server" id="TextBox1" text="Type here" />
Here's the typical code you get for the preceding markup:
Private Function __BuildControlTextBox1() As TextBox
Dim __ctrl As New TextBox()
Me.TextBox1 = __ctrl
__ctrl.ApplyStyleSheetSkin(Me)
__ctrl.ID = "TextBox1"
__ctrl.Text = "Type here"
Return __ctrl
End Function
What if the control has an event handler or a data binding expression? Let's first consider a button with the Click event handler. You need one additional line:
__ AddHandler __ctrl.Click, AddressOf Me.Button1_Click
For data binding expressions <%# ... %>, the code generated is similar except that the DataBinding event is used:
AddHandler __ctrl.DataBinding, AddressOf Me.DataBindingMsg
The code associated with the handler depends on the nature of the bound control and the code being bound. For the Text property of a Label control it looks like this:
Public Sub DataBindingMsg(ByVal sender As Object, ByVal e As EventArgs)
Dim target As Label = DirectCast(sender, Label)
target.Text = Convert.ToString(..., _
CultureInfo.CurrentCulture);
End Sub
The expression passed to Convert.ToString is exactly the code in the <%# ... %> expression. The type casting is also dependent on the types involved.
If Master Pages and themes are present, the number of source files and the list of dependencies grow, but with the ASP Explorer tool you can track them down at will.
Wrap-Up
ASP.NET performs on-demand dynamic code compilation of the resource types it owns. This feature greatly facilitates rapid iterative development of Web applications but it does require ASP.NET to be able to write files to disk. The compilation folder is a critical folder where much of the ASP.NET magic takes place. You can peer into it for fun-and sometimes even to diagnose and debug tough problems. Of course, most of the functionality discussed here is internal to ASP.NET and, as such, might change in future builds without being noted. As of today, though, that's how ASP.NET 2.0 works. By the way, feel free to use the ASP Explorer tool with ASP.NET AJAX applications too. It works just fine.
Send your questions and comments for Dino to cutting@microsoft.com.
Dino Esposito is a mentor at Solid Quality Learning and the author of Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005). Based in Italy, Dino is a frequent speaker at industry events worldwide. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.