In this article I'm going to describe the bridge between managed and unmanaged worlds and how the two collaborate to set up the processing environment needed by ASP.NET requests to be processed.

Introduction

In the previous as well as first article of this series I've introduced the first steps performed by a generic web request once accepted by the web server, and what route it takes when it is identified as an ASP.NET resource request. You've seen how different releases of IIS deal with incoming ASP.NET-related requests until they finally dispatch them to an unmanaged Win32 component called aspnet_isapi.dll, which acts as a bridge between the web server and the managed ASP.NET infrastructure.

In this article I'm going to resume the discussion exactly where I left it and start delving into the managed part of the processing environment I've presented in the previous article as those black boxes labeled ASP.NET Http Runtime Environment.

Note: The ASP.NET Http Runtime Environment is sometimes also referred to as the ASP.NET Pipeline, Http Runtime Pipeline or a mix of these words.

Leaving the aspnet_isapi.dll extension for the managed world

In the previous article I've explained how IIS 5 and IIS 6 manage ASP.NET requests. Regardless of how they deal with worker processes generation, management and recycling, all the information pertaining to the request is in the end forwarded to the aspnet_isapi.dll extension.

This is the bridge between the unmanaged and managed world and is the least documented part of all the ASP.NET architecture.

Note: At the moment of the writing of this article IIS 7 has just been released with a Go Live license along with Longhorn Server Beta 3. A lot of things will change with IIS 7, and although these first articles have been focusing solely on previous versions of IIS, a future article will be dedicated to changes and improvements introduced by IIS 7.

As for the lack of documentation available on most parts of this topic, what I'm going to explain may not be completely correct, especially for what concerns the unmanaged parts of the infrastructure. However, my considerations on the undocumented topics are based on some tools which helped a lot in understanding and making sense of the inner workings of the framework:

  • Lutz Roeder's .NET Reflector, to analyze statically and decompile the code of .NET managed classes.
  • Microsoft CLR Profiler, to analyze the dynamic behavior of the framework by looking at method calls, memory allocation, class instantiation and a whole big number of other features it provides.
  • JetBrains dotTrace Profiler, a commercial profiler for .NET Applications. A free time limited version is available too.
  • Red Gate ANTS Profiler, another commercial profiler for .NET applications. A free time limited version is available.

Going back to the bridge between the unmanaged and managed worlds, once the CLR has been loaded - either by the ISAPI Extension in case of IIS 6 process model or by the worker process in case of IIS 5 process model - the black magic occurs through a bunch of unmanaged COM interfaces calls that the ASP.NET ISAPI component makes to a couple of managed classes contained in the System.Web.Hosting namespace, the AppManagerAppDomainFactory class and the ISAPIRuntime class, which expose some methods via COM-callable interfaces.

Note: The Common Language Runtime - CLR - represents the execution environment of every .NET application. It's what actually provides both environment and services for running managed applications. It has to be hosted inside a Win32 process and ASP.NET is one of the available hosts for the CLR provided by the .NET framework. More specifically, the ASP.NET worker process (aspnet_wp.exe in IIS 5 and w3wp.exe in IIS 6) is the process who hosts the CLR in ASP.NET.

Before delving into the technical details of the interactions occurring between these classes let's see on the surface what actually happens for a request to be processed. I have introduced the previous two classes because the entry points in the processing of a request can be roughly grouped into two categories:

  1. Setup of an AppDomain, in case one doesn't exist yet, to represent the application to which the request is targeted - this is accomplished by the AppManagerAppDomain class.
  2. Handling and processing of the request - accomplished by the ISAPIRuntime class.

Though the two categories are equally important, the first consists of interactions which are supposed to happen without the interference of the developer and which concern mostly the hosting environment of the application, while the second is the most configurable part of the infrastructure, which I will delve into completely in the following article of this series.

Under another point of view, the first category comprises operations performed only once during the lifecycle of an application, that is, at startup, while the second consists of interactions occurring at each request targeting that specific application.

Setting up an AppDomain

As seen in the previous article, an ASP.NET application is reserved and wrapped into an entity called Application Domain, aka AppDomain, which turns out to be represented by a class of the ASP.NET architecture, the AppDomain class. When a request to a particular application arrives, an AppDomain has to be setup, if it doesn't exist. This usually happens if the incoming request is the first one targeting that particular application, or if for some reasons the corresponding AppDomain has been shut down, which may happen for several reasons that I will talk about later. Note that only one AppDomain exists for a single ASP.NET application, which is itself mapped one-to-one with an IIS application - either created over a physical/virtual directory. So how does an instance of this class get created?

Figure 1: Call stack generated by the creation of an AppDomain instance as shown by JetBrains dotTrace Profiler

AppDomain
(Click for larger image)

Using Reflector it's possible to realize that the AppDomain class has a private constructor which throws a NotSupportedException exception. Therefore this is obviously not the way to go. The entry point for the instantiation of an AppDomain is in fact the AppManagerAppDomainFactory.Create method. To find it out it's necessary to use a profiler which keeps track of the call stack generated when instantiating the object. Figure 1 displays a screenshot of JetBrains dotTrace Profiler, which shows the call stack generated to instantiate the AppDomain of a web application hosted on IIS. A lot of classes participate in the process, but it's something a developer will probably never have to put his hands on.

Note that the AppManagerAppDomainFactory class is instantiated only once during and by the CLR initialization process. As its name suggests, it's used as the entry point for creating AppDomains as well as other critical objects.

Figure 2 shows the same call stack as the previous image, this time captured from the output of the Call Tree view of Microsoft CLR Profiler. This provides some more information about the instantiation of the AppDomain. Actually, the highlighted line shows that there are two instances of the AppDomain class created by the principal thread.

Figure 2: Call stack generated by the creation of an AppDomain instance as shown by the Call Tree view of Microsoft CLR Profiler

AppDomain
(Click for larger image)

So, how does this additional instance get created, and why? Unluckily it's not an easy question. The call stack shown in the previous images represents the steps followed for instantiating the AppDomain which corresponds to the actual application being executed, so this additional instance looks like a helper object used for some purpose which is difficult to understand - as far as I can guess, it's used to host all the objects which don't belong to a specific application, and therefore are hosted in an isolated AppDomain, like the AppManagerAppDomainFactory class. As for the AppManagerAppDomainFactory class, this helper AppDomain is instantiated only once during and by the CLR initialization, and very very early during this stage, as shown in Figure 3.

Figure 3: Order and allocation method of the additional AppDomain instance

AppDomain instantiation

Figure 3 represents a screenshot of Red Gate ANTS Profiler. It shows that the additional AppDomain instance is created very early and during the initialization of the CLR. It is obviously created earlier than the AppManagerAppDomainFactory class, since it is probably its container. The ID of the singleton AppManagerAppDomainFactory instance, in fact, in the profiling session of the previous figure turns out to be 52.

Another way to see the instantiation process of the two AppDomains - which I present for completeness - is the Allocation Graph view provided by Microsoft CLR Profiler. It creates a graphical flow representation of the classes taking place in the process, as shown in Figure 4.

Figure 4: Instantiation of two AppDomains shown with the Allocation Graph view of Microsoft CLR Profiler

AppDomain - Allocation Graph

This represents a compressed view which cuts out all the classes between the <root> element - the CLR - and the Thread class. However, it shows clearly the two routes followed when creating the two AppDomain instances.

Another information about AppDomain instances that can be obtained from the previous figures is that each instance occupies exactly 100 bytes in memory. Not that it matters much, but one might think that an AppDomain is a huge class since it has to host an entire application. Actually, it provides a lot of services but doesn't store a lot of data.

By now, the helper AppDomain instance has been created, as well as an instance of the AppManagerAppDomainFactory class, whose Create method is the entry point of the call stack shown in figures 1 and 2.

This method is exposed and called via COM, thus unmanaged code. The AppManagerAppDomainFactory class implements the IAppManagerAppDomainFactory interface, whose structure is represented in Figure 5.

Figure 5: Structure of the IAppManagerAppDomainFactory interface

IAppManagerAppDomainFactory

The interface is decorated with the ComImport and InterfaceType attribute, which bind the type to an unmanaged interface type. When this method is called it produces the call stack of figures 1 and 2 which in the end triggers the creation of an instance of the AppDomain class, the one used to host the web application targeted by the request.

Once the AppDomain is up and running all that's left to do - and it's much more than what's been done so far - is processing the request. This is probably the most interesting part of the ASP.NET architecture, but before approaching the fun part I wanted to introduce some core topics.

Summary

In this article I've introduced some very low level topics about the ASP.NET infrastructure, those concerning the interface between the unmanaged world - represented by the ASP.NET ISAPI extension - and the managed world, represented by the AppDomain hosting the web application. I've presented the mechanisms by which an AppDomain gets instantiated and which classes take part in the process. In the next article I will discuss of managed code only by presenting the HTTP Pipeline, which is in my opinion the most attractive chapter of the ASP.NET architecture, and what actually makes ASP.NET different from all the other web development frameworks out there. I hope you enjoyed reading this article as much as I did writing it. My advice is to fire up some profiler and try out this stuff by yourself, which is the best way to fully understand how it all works.

References

posted on 2008-04-17 18:22  Liu Jian  阅读(394)  评论(0编辑  收藏  举报