Well done is better than well said.
-Benjamin Franklin
In various passages throughout this book, I've mentioned HTTP modules and HTTP handlers and introduced them as fundamental pieces of the ASP.NET architecture. HTTP handlers and modules are truly the building blocks of the .NET Web platform. While a request for a page or a Web service is being processed, it passes through a pipeline of HTTP modules, such as the authentication module and the session module, and is then processed by a single HTTP handler. After the handler has processed the request, the request flows back through the pipeline of HTTP modules and is finally transformed into HTML code for the browser.
An HTTP handler is the component that actually takes care of serving the request. It is an instance of a class that implements the IHttpHandler interface. The ProcessRequest method of the interface is the central console that governs the processing of the request. For example, the Page class implements the IHttpHandler interface, and its ProcessRequest method is responsible for loading and saving the view state and for firing the events such as Init, Load, PreRender, and the like.
ASP.NET maps each incoming HTTP request to a particular HTTP handler. A special breed of component—named the HTTP handler factory—provides the infrastructure for creating the physical instance of the handler to service the request. For example, the PageHandlerFactory class parses the source code of the requested .aspx resource and returns a compiled instance of the class that represents the page. An HTTP handler is designed to process one or more URL extensions. Handlers can be given an application or machine scope, meaning that they can process the assigned extensions within the context of the current application or all applications installed on the machine.
HTTP modules are classes that implement the IHttpModule interface and handle runtime events. There are two types of public events that a module can deal with. They are the events raised by HttpApplication (including asynchronous events) and events raised by other HTTP modules. For example, the SessionStateModule is provided by ASP.NET to supply session state services to an application. It fires the End and Start events that other modules can handle through the familiar Session_End and Session_Start signatures.
HTTP handlers and HTTP modules have the same functionality as ISAPI extensions and ISAPI filters, respectively, but with a much simpler programming model. ASP.NET allows you to create custom handlers and custom modules. Before we get into this rather advanced aspect of Web programming, a review of the Internet Information Services (IIS) extensibility model is in order because this model determines what modules and handlers can do and what they cannot do.
A Web server is primarily a server application that can be contacted using a bunch of Internet protocols such as HTTP, File Transfer Protocol (FTP), Network News Transfer Protocol (NNTP), and the Simple Mail Transfer Protocol (SMTP). IIS—the Web server included with the Windows operating system—is no exception.
A Web server also provides a documented API for enhancing and customizing the server's capabilities. Historically speaking, the first of these extension APIs was the Common Gateway Interface (CGI). A CGI module is a brand new application that is spawned from the Web server to service a request. Nowadays CGI applications are almost never used in modern Web applications because they require a new process for each HTTP request. As you can easily understand, this approach is rather inadequate for high-volume Web sites and poses severe scalability issues. IIS supports CGI applications, but you seldom exploit this feature unless you have serious backward compatibility issues. More recent versions of Web servers supply an alternative and more efficient model to extend the capabilities of the module. In IIS, this alternative model takes the form of the Internet Server Application Programming Interface (ISAPI).
When the ISAPI model is used, instead of starting a new process for each request, IIS loads an ISAPI component—namely, a Win32 DLL—into its own process. Next, it calls a well-known entry point on the DLL to serve the request. The ISAPI component stays loaded until IIS is shut down and can service requests without any further impact on the Web server activity. The downside to such a model is that because components are loaded within the Web server process, a single faulty component can tear down the whole server and all installed applications. Starting with IIS 4.0, though, some countermeasures have been taken to alleviate this problem. As a result, while installing a Web application, you can control the protection level through the dialog box shown in Figure 23-1.
Figure 23-1: Configuring the protection level of a Web application in IIS 5.0.
If you choose a low protection, the application (and its extensions) will be run within the Web server process (Inetinfo.exe). If you choose medium protection, applications will be pooled together and hosted by an instance of a different process (Dllhost.exe). If you chooses high protection, each application set to High will be hosted in its own individual process (Dllhost.exe). Figure 23-2 shows the same dialog box in IIS 6.0 and Windows Server 2003.
Figure 23-2: Configuring the protection level of Web applications in IIS 6.0 under Windows Server 2003.
As we mentioned in Chapter 2, Web applications under IIS 6.0 are grouped in pools and the choice you can make is whether you want to join an existing pool or create a new one. (See Figure 23-3.)
Figure 23-3: A view of the application pools in IIS 6.0.
Illustrious Children of the ISAPI Model
Aside from performance and security considerations, the ISAPI model has another key drawback—the programming model. An ISAPI component represents a compiled piece of code—a Win32 DLL—that retrieves data and writes HTML code to an output console. It has to be developed using C or C++, it should generate multithreaded code, and it must be written with extreme care because of the impact that bugs or runtime failures can have.
Microsoft attempted to encapsulate the ISAPI logic in the Microsoft Foundation Classes (MFC) but, although creditable, that effort didn't pay off very well. Active Server Pages (ASP), the predecessor of ASP.NET, is indeed an example of a well-done ISAPI application. ASP is implemented as an ISAPI DLL (named asp.dll) registered to handle HTTP requests with an .asp extension. The internal code of the ASP ISAPI parses the code of the requested resource, executes any embedded script code, and builds the page for the browser.
Any functionality built on top of IIS must be coded using the ISAPI model. ASP and ASP.NET are no exceptions. The ASP.NET worker process—the module that hosts the common language runtime (CLR) and the runtime pipeline (see Chapter 2)—is an executable connected to IIS by an ISAPI component—the aspnet_isapi.dll. When a request for .aspx or .asmx resources comes in, IIS passes the control to the ISAPI module which, in turn, hands the request to the worker process. As of this writing, to extend IIS you can only write a Win32 DLL with a well-known set of entry points.
Structure of ISAPI Components
An ISAPI extension is invoked through a URL that ends with the name of the DLL library that implements the function, as shown in the following URL:
http://www.contoso.com/apps/hello.dll
The DLL must export a couple of functions—GetExtensionVersion and HttpExtensionProc. The GetExtensionVersion function sets the version and the name of the ISAPI server extension. When the extension is loaded, the GetExtensionVersion function is the first function to be called. GetExtensionVersion is invoked only once and can be used to initialize any needed variables. The function is expected to return true if everything goes fine. In case of errors, the function should return false and the Web server will abort loading the DLL and put a message in the system log.
The core of the ISAPI component is represented by the HttpExtensionProc function. The function receives basic HTTP information regarding the request (for example, the query string and the headers), performs the expected action, and prepares the response to send back to the browser.
Important |
Certain handy programming facilities, such as the session object, are abstractions the ISAPI programming model lacks. The ISAPI model is a lower-level programming model than, say, ASP or ASP.NET scripting. |
The ISAPI programming model is made of two types of components—ISAPI extensions and ISAPI filters.
ISAPI extensions are the IIS in-process counterpart of CGI applications. As mentioned, an ISAPI extension is a DLL that is loaded in the memory space occupied by IIS or another host application. Because it is a DLL, only one instance of the ISAPI extension needs to be loaded at a time. On the downside, the ISAPI extension must be thread-safe so that multiple client requests can be served simultaneously. ISAPI extensions work in much the same way as an ASP page does. It takes any information about the HTTP request and prepares a valid HTTP response.
Because the ISAPI extension is made of compiled code, it must be recompiled and reloaded at any change. If the DLL is loaded in the Web server's memory, this means that the Web server must be stopped. If the DLL is loaded in the context of a separate process, only that process must be stopped. Of course, when an external process is used, the extension doesn't work as fast as it could when hosted in-process, but at least it doesn't jeopardize the stability of IIS.
While ISAPI extensions are the counterpart of CGI applications, ISAPI filters can be considered a relatively new concept in Web server extensibility. ISAPI filters are components that intercept specific server events before the server itself handles them. Upon loading, the filter indicates what event notifications it will handle. If any of these events occur, the filter can process them or pass them on to other filters.
You can use ISAPI filters to provide custom authentication techniques or to automatically redirect requests based on HTTP headers sent by the client. Filters are a delicate gear in the IIS machinery. They can facilitate applications but also degrade performance if not written carefully. Filters, in fact, can only run in-process. With IIS 5.0, filters can be loaded for the Web server as a whole or for specific Web sites.
ISAPI filters can accomplish tasks such as implementing custom authentication schemes, compression, encryption, logging, and request analysis. The ability to examine, and if necessary modify, both incoming and outgoing streams of data makes ISAPI filters very powerful and flexible. This last sentence gives the measure of the strength of ISAPI filters but also indicates the potential weakness of them.
ASP.NET can be seen as a microcosm within the universe of IIS. The point of contact between the two worlds is the aspnet_isapi.dll component. The component is a Win32 DLL acting as an ISAPI extension. It handles requests for .aspx and .asmx resources (and a few more indeed) and then sets up the ASP.NET pipeline to service them. The ASP.NET pipeline, hosted by the ASP.NET worker process aspnet_wp.exe, consists of managed code and implements its own extensibility model made of HTTP handlers and HTTP modules. Although HTTP handlers look like ISAPI extensions, and HTTP modules are the managed counterpart of ISAPI filters, the resemblance makes sense only at a high level of abstraction, as Figure 23-4 shows.
Figure 23-4: ISAPI components and HTTP handlers and HTTP modules.
HTTP handlers and HTTP modules never directly interact with IIS. They are made of managed code, live within the space of the ASP.NET worker process, and can only affect ASP.NET applications. For example, using an HTTP module, you can't implement a custom authentication scheme for an ASP application. Likewise, if you want to define a custom type of resource (say, a family of URLs with the .sqlx suffix), you can use HTTP handlers as long as you bind it to aspnet_isapi. Any request to an .sqlx resource will be funneled to aspnet_isapi and from there to your handler.
An HTTP handler can work either synchronously or operate in an asynchronous way. When synchronous, a handler doesn't return until it's done with the HTTP request. An asynchronous handler, on the other hand, launches a potentially lengthy process and returns immediately after.
Conventional ISAPI extensions and filters must be registered within the IIS metabase. In contrast, HTTP handlers must be registered with aspnet_isapi using the application's or machine's configuration file.
Want to take the splash and dive into HTTP handler programming? Well, your first step is getting the hang of the IHttpHandler interface. An HTTP handler is just a managed class that implements that interface. More exactly, a synchronous HTTP handler implements the IHttpHandler interface; an asynchronous HTTP handler, on the other hand, implements the IHttpAsyncHandler interface. Let's tackle synchronous handlers first.
The contract of the IHttpHandler interface defines the actions that a handler needs to take to process an HTTP request synchronously. In the .NET Framework, three classes implement the interface, as shown in Table 23-1.
Handler |
Description |
---|---|
HttpApplication |
Base class for ASP.NET applications that implements both the synchronous and asynchronous interfaces. Instances of this class are created only by the ASP.NET runtime, not by user code, and are used to process all the requests for the application, but one at a time. Note that although the class implements IHttpHandler, it always serves requests asynchronously. The implementation of IHttpHandler doesn't result in operative code. |
HttpRemotingHandler |
Implements an ASP.NET handler that forwards requests to the remoting HTTP channel. This handler is registered to process requests for URLs that end with .soap or .rem. These are fake resources that identify a method call on a remote object when IIS is used as the .NET Remoting application host. |
Page |
Represents an .aspx file requested from a server that hosts an ASP.NET application. |
A noticeable absence in this table is that of a class that supports Web services. The reason such classes don't appear in the table is that they are nonpublic. However, Web services calls are handled by a bunch of internal handler classes—SyncSessionHandler and AsyncSessionHandler, which manage session-aware Web services, and SyncSessionlessHandler and AsyncSessionlessHandler, which serve requests for sessionless Web services.
Members of the IHttpHandler Interface
The IHttpHandler interface defines only two members—ProcessRequest and IsReusable, as shown in Table 23-2. ProcessRequest is a method whereas IsReusable is a Boolean property.
Member |
Description |
---|---|
IsReusable |
Gets a Boolean value indicating whether another request can use the current instance of the HTTP handler. |
ProcessRequest |
The method enables processing of HTTP requests. |
The IsReusable property on the Page class returns false, meaning that a new instance of the HTTP request is needed to serve a page request. The ProcessRequest method has the following signature:
void ProcessRequest(HttpContext context);
It takes the context of the request as the input and ensures that the request is serviced. In case of synchronous handlers, when ProcessRequest returns, the output is ready for being forwarded to the client.
ASP.NET Request Processing
Now that we know a little more about HTTP handlers, it would be useful to look back at some of the topics we covered in Chapter 2—in particular, the steps involved in the actual processing of an ASP.NET request. The entry point in the ASP.NET pipeline is the HttpRuntime object. The class has a static method named ProcessRequest, which drives the execution of all ASP.NET requests.
The ProcessRequest method receives information about the request packed in an HttpWorkerRequest class. This abstract class defines the base worker methods and enumerations used by ASP.NET managed code. In most cases, user code will not deal with HttpWorkerRequest directly but access request and response data through the HttpRequest and HttpResponse classes. However, if your code implements its own hosting environment, it will need to extend this class to call the ProcessRequest method. (We'll cover ASP.NET hosting in the next chapter.)
The HTTP runtime first updates the performance counter and then processes the request. Processing a request entails performing the following steps.
In brief, HTTP handlers are just what their name clearly states—managed components designed to handle HTTP requests and carry them out. HTTP handlers are invoked whatever the type of the requested resources is, as long as IIS routes the request to aspnet_isapi. Now let's examine the structure of a very simple synchronous HTTP handler.
A Very Simple HTTP Handler
As mentioned earlier, an HTTP handler is simply a class that implements the IHttpHandler interface. The output for the request is built within the ProcessRequest method, as shown in the following code:
using System.Web; namespace HandlerExample { public class SimpleHttpHandler : IHttpHandler { // Override the ProcessRequest method public void ProcessRequest(HttpContext context) { context.Response.Write("
"); } // Override the IsReusable property public bool IsReusable { get { return true; } } } }
We need an entry point to be able to call the method. In this context, an entry point into the code is nothing more than an HTTP endpoint—that is, a public URL. The URL must be a unique name that IIS and the HTTP runtime map to this code. The mapping between an HTTP handler and a Web server resource is established through the web.config file.
The section lists the handlers available for the current application. If you enter the settings just shown in the machine.config file, you will register the myHandler.aspx URL, which is callable from within all Web applications in the machine. These settings indicate that any incoming requests for myHandler.aspx must be handled by the specified HTTP handler. The URL myHandler.aspx doesn't have to be a physical resource; it's simply a public resource identifier. The type attribute references the class and assembly that contains the handler. If you invoke the myHandler.aspx URL, you obtain the results shown in Figure 23-5.
Figure 23-5: A sample HTTP handler that answers requests for myHandler.aspx.
The technique discussed here is the quickest and simplest way of putting an HTTP handler to work, but there is more to know about registration of HTTP handlers and there are more options to exploit. Now let's consider a more complex example of an HTTP handler.
With its relatively simple programming model, HTTP handlers give you a means of interacting with the low-level request and response services of IIS. In the previous example, we only returned constant text and made no use of the request information. In the next example, we'll configure the handler to intercept and process only requests of a particular type and generate the output based on the contents of the requested resource.
The idea is to build an HTTP handler for custom .sqlx resources. A SQLX file is an XML document that represents a SQL query. The handler grabs the information about the query, executes it, and finally returns the result set formatted as a grid. Figure 23-6 shows the expected outcome.
Figure 23-6: A custom HTTP handler in action.
To start, let's examine the source code for the IHttpHandler class.
Building a Query Manager Tool
The HTTP handler should get into the game whenever the user requests an .sqlx resource. Assume for now that this aspect has been fully configured, and focus on what's needed to execute the query and pack the results into a grid. To execute the query, at a minimum, we need the connection string and the command text. The following text illustrates the typical contents of an .sqlx file:
SELECT firstname, lastname FROM employees SELECT TOP 10 companyname FROM customers
The XML document is formed by a unique node—the node—that contains the connString attribute. The attribute stores the connection string, whereas the text of the node represents the query command.
The ProcessRequest method of the handler needs to extract this information before it can proceed with executing the query and generating the output.
class SqlxData { public string ConnectionString; public string QueryText; } public class QueryHandler : IHttpHandler { // Override the ProcessRequest method public void ProcessRequest(HttpContext context) { // Parses the SQLX file SqlxData _data = ParseFile(context); // Create the output as HTML string html = CreateOutput(_data); // Output the data context.Response.Write("
"); context.Response.Write(_data.QueryText); context.Response.Write("
"); context.Response.Write(html); context.Response.Write("
"); } // Override the IsReusable property public bool IsReusable { get { return true; } } }
The ParseFile helper function uses an XML reader to scan the source code of the .sqlx resource.
private SqlxData ParseFile(HttpContext context) { SqlxData _data = new SqlxData(); // Get the name of the file to process string _file = context.Server.MapPath(context.Request.Path.ToString()); // Go to node and get the connection string string _connString = null; XmlTextReader _reader = new XmlTextReader(_file); _reader.Read(); _reader.MoveToContent(); _connString = _reader["connString"]; if (_connString == null) { _reader.Close(); context.Response.Write(ErrorMessage(_file)); } // Go to the content node and get the command text string _queryText = null; _reader.Read(); _queryText = _reader.Value; if (_queryText == null) { _reader.Close(); context.Response.Write(ErrorMessage(_file)); } // Close the reader _reader.Close(); // Pack the data _data.ConnectionString = _connString; _data.QueryText = _queryText; return _data; }
The SqlxData internal class groups the connection string and the command text. The information is passed to the CreateOutput function, which will actually execute the query and generate the grid.
private string CreateOutput(SqlxData data) { // Run the query DataSet ds = new DataSet(); SqlDataAdapter adapter = new SqlDataAdapter(data.QueryText, data.ConnectionString); adapter.Fill(ds); // Prepare a distinct grid for each result set StringWriter sw = new StringWriter(); HtmlTextWriter writer = new HtmlTextWriter(sw); for(int i=0; i"); } return sw.ToString(); }
After executing the query, the method populates a dynamically created DataGrid control. In ASP.NET pages, the DataGrid control, like any other control, is rendered to HTML. However, this happens through the care of the special HTTP handler that manages .aspx resources. For .sqlx resources, we need to provide that functionality ourselves. Obtaining the HTML for a Web control is as easy as calling the RenderControl method on an HTML text writer object. This is just what the page HTTP handler does by using the response stream as the underlying data store. In this case, though, we need to obtain the output as a string; so you use a StringWriter object as the back-end data store. This technique to obtain the HTML text generated for a control is quite general and can be used in a variety of circumstances and with all Web controls.
The handler generates a grid for each result set returned by the query in the .sqlx file.
Note |
An HTTP handler that needs to access session state values must implement the IRequiresSessionState interface. Like INamingContainer, it's a marker interface and requires no method implementation. Note that the IRequiresSessionState interface indicates that the HTTP handler requires read and write access to the session state. If read-only access is needed, use the IReadOnlySessionState interface instead. |
Registering the Handler
An HTTP handler is a class and must be compiled to an assembly before you can use it. The assembly must be deployed to the Bin directory of the application. If you plan to make this handler available to all applications, you can copy it to the global assembly cache (GAC). The next step is registering the handler with an individual application or with all the applications running on the Web server. You register the handler in the configuration file.
You add the new handler to the section of the local or global web.config file. The section supports three actions: , , and . You use to add a new HTTP handler to the scope of the .config file. You use to remove a particular handler. Finally, you use to get rid of all the registered handlers. To add a new handler, you need to set three attributes: verb, path, and type, as shown in Table 23-3.
Attribute |
Description |
---|---|
verb |
Indicates the list of the supported HTTP verbs. For example, GET, PUT, and POST. The wildcard character (*) is an acceptable value and denotes all verbs. |
path |
A wildcard string, or a single URL, that indicates the resources the handler will work on. For example, *.aspx. |
type |
Specifies a comma-separated class/assembly combination. ASP.NET searches for the assembly DLL first in the application's private Bin directory and then in the system global assembly cache. |
These attributes are mandatory. An optional attribute is also supported—validate. When validate is set to false, ASP.NET will delay as much as possible loading the assembly with the HTTP handler. In other words, the assembly will be loaded only when a request for it arrives. ASP.NET will not try to preload the assembly, thus catching any error or problem with it. The configuration script previously shown registers the QueryHandler class in the QueryHandler.dll assembly to handle all HTTP verbs that involve *.sqlx resources.
So far you have correctly deployed and registered the HTTP handler, but if you try invoking an .sqlx resource, the results you produce are not what you'd expect. The problem lies in the fact that so far you configured ASP.NET to handle only .sqlx resources; but IIS still doesn't know anything about them!
A request for an .sqlx resource is managed by IIS in the first place. If you don't register an ISAPI extension to handle that, IIS will treat it as a static resource and serve the request by sending back the source code of the file. The extra step required is registering the .sqlx extension with the IIS metabase, as shown in Figure 23-7.
Figure 23-7: Registering the .sqlx extension with the IIS metabase.
To involve the HTTP handler, you must choose aspnet_isapi.dll as the ISAPI extension. In this way, all .sqlx requests are handed out to ASP.NET and processed through the specified handler.
Note |
If your Web application manages resources of a type that you don't want to make publicly available over the Web, you must instruct IIS not to display those files. A possible way to accomplish this consists of forwarding the request to aspnet_isapi and then binding the extension to one of the internal handlers—the HttpForbiddenHandler class. Any attempt to access an .xyz resource results in an error message being displayed. |
Deploying as an ASHX Resource
An alternative way to define an HTTP handler is through an .ashx file. When a request for a specified .ashx resource is received, the handler will get executed. The association between the HTTP handler and the .ashx resource is established within the .ashx source file by using a special directive: @WebHandler. All .ashx files must begin with a directive like the following:
<%@ WebHandler Language="C#" %>
When the URL that corresponds to the .ashx resource is invoked, the MyHandler class is automatically invoked.
When .ashx resources are used to implement an HTTP handler, no deployment setup is needed. In particular, you don't need to arrange for a web.config file and for a new section. You just deploy the source file, and you're done. Just as for Web services, the source file will be loaded and compiled only on demand. Because ASP.NET adds a special entry to the IIS metabase for .ashx resources, you don't even need to enter changes to the Web server configuration.
As we discussed in Chapter 2, the machine.config file defines a few standard HTTP handlers for commonly accessed resource types. In particular, handlers for .aspx, .asmx, and .ashx resources are defined. The former two types serve requests for Web pages and Web services. The latter type is handled by an HTTP handler class named SimpleHandlerFactory. Figure 23-8 illustrates the role of the SimpleHandlerFactory class in the processing of .ashx resources. Note that SimpleHandlerFactory is actually an HTTP handler factory class, not a simple HTTP handler class. We'll discuss handler factories in a moment.
Figure 23-8: The @WebHandler directive within an .ashx resource tells the HTTP handler factory about the HTTP handler class to instantiate.
The SimpleHandlerFactory class looks for the @WebHandler directive at the beginning of the file. The @WebHandler directive tells the handler factory the name of the HTTP handler class to instantiate once the source code has been compiled.
Important |
You should resort to .ashx resources to implement application-specific functionality that needs to be processed faster than regular Web pages. In any case, an HTTP handler returns a valid HTTP response with a content type and a body. Serving an .ashx request results in faster code than serving an .aspx resource. Processing an .ashx resource is faster because no intermediate events are raised to the user code (for example, Init, Load), no view state is managed, and no postback mechanism is supported. Roughly speaking, an .ashx request is like an .aspx resource in which only the rendering step ever takes place. Should you use generic .ashx resources or go for a custom extension? That mostly depends on the function you want to implement. The ASHX approach is designed for relatively simple scenarios in which you have few parameters to pass (or no parameters at all) and for which you use the query string to bring them in. A custom extension is preferable if you have a custom document to process with a variety of information organized in a nonflat layout. |
In a conventional HTTP handler, the ProcessRequest method takes the lion's share of the overall set of functionality. The second member of the IHttpHandler interface—the IsReusable property—is used only in particular circumstances. By calling IsReusable, an HTTP handler factory can query a handler to determine whether the same instance can be used to service multiple requests.
The Boolean value returned by IsReusable also indicates whether the handler object can be pooled. This is certainly true for HttpApplication objects called to serve a request, but it's patently false for handlers representing a Web Forms page. If you set the IsReusable property to return true, the handler is not unloaded from memory after use.
In all the cases considered so far, we never had the need to distinguish between two or more distinct handlers for a request. In situations in which the HTTP handler for a request is not uniquely identified, you might want to use an HTTP handler factory object.
HTTP Handler Factories
An HTTP request can be directly associated with an HTTP handler or with an HTTP handler factory object. An HTTP handler factory is a class that implements the IHttpHandlerFactory interface and is in charge of returning the actual HTTP handler to use to serve the request. For example, the SimpleHandlerFactory class handles requests for .ashx resources and determines the right handler to use by looking at the @WebHandler directive in the source file.
In the .NET Framework, HTTP handler factories are used to perform some preliminary tasks on the requested resource prior to passing it on to the handler. A typical handler factory object is represented by the internal class named PageHandlerFactory. It figures out the name of the handler to use and, if possible, loads it up from an existing assembly. In the SQLX example, we could use a handler factory class to insulate the code necessary to process the file and extract the contained information.
HTTP handler factories are classes that implement a couple of methods on the IHttpHandlerFactory interface—GetHandler and ReleaseHandler, as shown in Table 23-4.
Method |
Description |
---|---|
GetHandler |
Returns an instance of an HTTP handler to serve the request. |
ReleaseHandler |
Takes an existing HTTP handler instance and frees it up or pools it. |
The GetHandler method has the following signature:
public virtual IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated);
The requestType argument is a string that evaluates to GET or POST—the HTTP verb of the request. The last two arguments represent the raw URL of the request and the physical path behind it. The ReleaseHandler method is a mandatory override for any class that implements IHttpHandlerFactory; in most cases, it will just have an empty body.
The following listing shows an example HTTP handler factory that returns different handlers based on the HTTP verb (GET or POST) used for the request:
namespace ProAspNet.CS.Ch23 { class MyHandlerFactory : IHttpHandlerFactory { public IHttpHandler GetHandler(HttpContext context, string requestType, String url, String pathTranslated) { if(context.Request.RequestType.ToLower() == "get") return (IHttpHandler) new MyGetHandler(); else if(context.Request.RequestType.ToLower() == "post") return (IHttpHandler) new MyPostHandler(); return null; } public void ReleaseHandler(IHttpHandler handler) { // nothing to do } } }
When you use an HTTP handler factory, it's the factory, not the handler, that needs to be registered with the ASP.NET configuration file. If you register the handler, it will always be used to serve requests. If you opt for a factory, you have a chance to decide dynamically and based on runtime conditions which handler is more appropriate for a certain request.
Asynchronous Handlers
An asynchronous HTTP handler is a class that implements the IHttpAsyncHandler interface. The system initiates the call by invoking the BeginProcessRequest method. Next, when the method ends, a callback function is automatically invoked to terminate the call. In the .NET Framework, the sole HttpApplication class implements the asynchronous interface. The members of IHttpAsyncHandler interface are shown in Table 23-5.
Method |
Description |
---|---|
BeginProcessRequest |
Initiates an asynchronous call to the specified HTTP handler. |
EndProcessRequest |
Terminates the asynchronous call. |
The signature of the BeginProcessRequest method is as follows:
IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
The context argument provides references to intrinsic server objects used to service HTTP requests. The second parameter is the AsyncCallback object to invoke when the asynchronous method call is complete. The third parameter is a generic cargo variable that contains any data you might want to pass to the handler.
Note |
An AsyncCallback object is a delegate that defines the logic needed to finish processing the asynchronous operation. A delegate is a class that holds a reference to a method. A delegate class has a fixed signature, and it can hold references only to methods that match that signature. A delegate is equivalent to a type-safe function pointer or a callback. As a result, an AsyncCallback object is just the code that executes when the asynchronous handler has completed its job. |
The AsyncCallback delegate has the following signature:
public delegate void AsyncCallback(IAsyncResult ar);
It uses the IAsyncResult interface to obtain the status of the asynchronous operation. To illustrate the plumbing of asynchronous handlers, let's examine the pseudocode that the HTTP runtime employs when it deals with asynchronous handlers. The HTTP runtime invokes the BeginProcessRequest method as follows:
// Sets an internal member of the HttpContext class with the current // instance of the asynchronous handler context.AsyncAppHandler = asyncHandler; // Invokes the BeginProcessRequest method on the asynchronous HTTP handler asyncHandler.BeginProcessRequest(context, OnCompletionCallback, context);
The context argument is the current instance of the HttpContext class and represents the context of the request. A reference to the HTTP context is also passed as the custom data destined to the handler to process the request. The extraData parameter in the BeginProcessRequest signature is used to represent the status of the asynchronous operation. The BeginProcessRequest method returns an object of type HttpAsyncResult—a class that implements the IAsyncResult interface. The IAsyncResult interface contains a property named AsyncState that is set with the extraData value—in this case, the HTTP context.
The OnCompletionCallback method is an internal method. It gets automatically triggered when the asynchronous processing of the request terminates. The following listing illustrates the pseudocode of the HttpRuntime private method:
// The method must have the signature of an AsyncCallback delegate private void OnHandlerCompletion(IAsyncResult ar) { // The ar parameter is an instance of HttpAsyncResult HttpContext context = (HttpContext) ar.AsyncState; // Retrieves the instance of the asynchronous HTTP handler // and completes the request IHttpAsyncHandler asyncHandler = context.AsyncAppHandler; asyncHandler.EndProcessRequest(ar); // Finalizes the request as usual }
The completion handler retrieves the HTTP context of the request through the AsyncState property of the IAsyncResult object it gets from the system. As mentioned, the actual object passed is an instance of the HttpAsyncResult class—in any case, it is the return value of the BeginProcessRequest method. The completion routine extracts the reference to the asynchronous handler from the context and issues a call to the EndProcessRequest method.
void EndProcessRequest(IAsyncResult result);
The EndProcessRequest method takes the IAsyncResult object returned by the call to BeginProcessRequest. As implemented in the HttpApplication class, the EndProcessRequest method does nothing special and is limited to throwing an exception if an error occurred.
So we've learned that ASP.NET runs as an ISAPI extension within the IIS process. Any incoming requests for ASP.NET resources is handed over to the ASP.NET worker process for the actual processing within the context of the CLR. Both in IIS 5.0 and 6.0, the worker process is a distinct process from IIS, so if one ASP.NET application crashes, it doesn't bring down the whole server. (In IIS 6.0, the handing over between the Web server and the worker process is more direct and faster.) ASP.NET manages a pool of HttpApplication objects for each running application and picks up one of the pooled instances to serve a particular request. As discussed in Chapter 13, these objects are based on the class defined in your global.asax file or on the base HttpApplication class if global.asax is missing. The goal of the HttpApplication object in charge of the request is getting an HTTP handler.
On the way to the final HTTP handler, the HttpApplication object makes the request pass through a pipeline of HTTP modules. An HTTP module is a .NET Framework class that implements the IHttpModule interface. The HTTP modules that filter the raw data within the request are configured on a per-application basis within the web.config file. All ASP.NET applications, though, inherit a bunch of system HTTP modules configured in the machine.config file. In Chapter 13, we discussed and commented on the default modules that provide features such as authentication, authorization, and session-related services.
Generally speaking, an HTTP module can pre-process and post-process a request, and it intercepts and handles system events as well as events raised by other modules. The good news is that you can write and register your own HTTP modules and make them plug into the ASP.NET runtime pipeline, handle system events, and fire their own events.
The IHttpModule interface defines only two methods—Init and Dispose. The Init method initializes a module and prepares it to handle requests. At this time, you subscribe to receive notifications for the events of interest. The Dispose method disposes of the resources (all but memory!) used by the module. Typical tasks you perform within the Dispose method are closing database connections or file handles.
The IHttpModule methods have the following signatures:
void Init(HttpApplication app); void Dispose();
The Init method receives a reference to the HttpApplication object that is serving the request. You can use this reference to wire up to system events. (See Chapter 13 for the complete list of system events.) The HttpApplication object also features a property named Context that provides access to the intrinsic properties of the ASP.NET application. In this way, you gain access to Response, Request, Session, and the like.
Let's come to grips with HTTP modules and write a custom module named Marker that adds a signature at the beginning and end of each page served by the application. The following code outlines the class we need to write:
using System; using System.Web; namespace ProAspNet.CS.Ch23 { public class MarkerModule : IHttpModule { public void Init(HttpApplication app) { // Register for pipeline events } public void Dispose() { // Nothing to do here } } }
The Init method is invoked by the HttpApplication class to load the module. In the Init method, you normally don't need to do more than simply register your own event handlers. The Dispose method is, more often than not, empty. The heart of the HTTP module is really in the event handlers you define.
Wiring Up Events
The sample Marker module registers a couple of pipeline events—BeginRequest and PreSendRequestContent. BeginRequest is the first event that hits the HTTP application object when the request begins processing. PreSendRequestContent is the event that signals the next sending of the response, and it's your last chance to modify the contents for the browser. By handling these two events, you can write custom text to the output stream before and after the regular HTTP handler—the Page-derived class.
The following listing shows the implementation of the Init and Dispose methods for the sample module:
public void Init(HttpApplication app) { // Register for pipeline events app.BeginRequest += new EventHandler(OnBeginRequest); app.PreSendRequestContent += new EventHandler(OnPreSendRequestContent); } public void Dispose() { }
The BeginRequest and PreSendRequestContent event handlers have a similar structure. They obtain a reference to the current HttpApplication object from the sender and get the HTTP context from there. Next, they work with the Response object to append text or a custom header.
public void OnBeginRequest(object sender, EventArgs e) { HttpApplication app = (HttpApplication) sender; HttpContext ctx = app.Context; // More code here // Add custom header to the HTTP response ctx.Response.AppendHeader("Author", "DinoE"); ctx.Response.Write(topText); } public void OnPreSendRequestContent(object sender, EventArgs e) { // Get access to the HTTP context HttpApplication app = (HttpApplication) sender; HttpContext ctx = app.Context; // More code here // Append some custom text ctx.Response.Write(bottomText); }
OnBeginRequest writes the page header and also adds a custom HTTP header. OnPreSendRequestContent simply appends the page footer. The effect of this HTTP module is visible in Figure 23-9.
Figure 23-9: The Marker HTTP module adds a header and footer to each page within the application.
The dockable window at the bottom of Internet Explorer is a free tool that snoops for the HTTP header being sent and received by the browser. The custom header in the response text is clearly visible.
Note that technically speaking the work done in BeginRequest could also go in the PreSendRequestHeaders handler, which occurs at the last possible moment before ASP.NET sends the headers out.
Note |
At this point, a good question to ask would be the following: why shouldn't we wire up the EndRequest event instead of PreSendRequestContent? As the names clearly suggest, the two events fire at very different moments. EndRequest indicates that the system is done with the request; PreSendRequestContent arrives when the system is going to flush the response buffer to the output stream. If your goal is simply to add a common footer to all responses a user gets out of a Web application, the two events are functionally equivalent. However, when PreSendRequestContent arrives, the buffer is still held by the Response's output stream object and can be modified before being sent to the browser. So by writing a handler for PreSendRequestContent, you can not only append a custom markup string but also modify the markup the handler has generated. |
Accessing Other HTTP Modules
The sample just discussed demonstrates how to wire up pipeline events—that is, events fired by the HttpApplication object. But what about events fired by other modules? The HttpApplication object provides a property named Modules that gets the collection of modules for the current application.
The Modules property is of type HttpModuleCollection and contains the names of the modules for the application. The collection class inherits from the abstract class NameObjectCollectionBase, which is a collection of pairs made of a string and an object. The string indicates the public name of the module; the object is the actual instance of the module. To access the module that handles the session state, you need code like this:
SessionStateModule sess = app.Modules["Session"]; sess.Start += new EventHandler(OnSessionStart);
You can also handle events raised by HTTP modules within the global.asax file and use the ModuleName_OnEventName convention to name the event handlers. The name of the module is just one of the settings you need to define when registering an HTTP module.
Registering with the Configuration File
You register a new HTTP module by adding an entry under the section of the configuration file. The overall syntax of the section closely resembles that of HTTP handlers. To add a new module, you use the node and specify the name and type attributes. The name attribute contains the public name of the module. This name will be used to select the module within the HttpApplication Modules collection. If the module fires custom events, this name is also used as the prefix for building automatic event handlers in the global.asax file.
The type attribute is the usual comma-separated string that contains the name of the class and the related assembly. The configuration settings can be entered into the application's configuration file as well as into the machine configuration file. In the former case, only pages within the application will be affected; in the latter case, all pages within all applications will be processed by the specified module.
The order in which modules are applied depends on the physical order of the modules in the configuration list. You can remove a system module and replace it with your own that provides a similar functionality. In this case, in the application's web.config file you use the node to drop the default module and then use to insert your own. If you want to completely redefine the order of HTTP modules for your application, you could clear all the default modules by using the node and then re-register them all in a different order.
HTTP handlers and HTTP modules are the building blocks of the ASP.NET platform. ASP.NET includes several predefined handlers and HTTP modules, but developers can write handlers and modules of their own to perform a variety of tasks. HTTP handlers, in particular, are faster than ordinary Web pages and can be used in all circumstances in which you don't need state maintenance and postback events. To generate images dynamically on the server, for example, an HTTP handler is more efficient than a page.
Everything that occurs under the hood of the ASP.NET runtime environment occurs because of HTTP handlers. When you invoke a Web page or a Web service method, an appropriate HTTP handler gets into the game and serves your request. At the highest level of abstraction, the behavior of an HTTP handler closely resembles that of an ISAPI extension. While the similarity makes sense, a couple of key differences exist. First and foremost, HTTP handlers are managed and CLR-resident components. The CLR, in turn, is hosted by the ASP.NET worker process. An ISAPI extension, on the other hand, is a Win32 library that can live within the IIS process. In the ASP.NET process model, the aspnet_isapi component is a true ISAPI extension that collects requests and dispatches them to the worker process. ASP.NET internally implements an ISAPI-like extensibility model in which HTTP handlers play the role of ISAPI extensions in the IIS world.
The second big difference between HTTP handlers and ISAPI extensions is that HTTP handlers provide a much simpler programming model. If you consider that just the programming model—overly complex, error-prone, and C++ oriented—is commonly believed as a major drawback of ISAPI, you should immediately grasp the importance of this ASP.NET innovation.
HTTP modules are to ISAPI filters what HTTP handlers are to ISAPI extensions. HTTP modules are good at performing a number of low-level tasks where a tight interaction and integration with the request/response mechanism is a critical factor. Modules are sort of interceptors that you can place along an HTTP packet's path, from the Web server to the ASP.NET runtime and back. Modules have read and write capabilities and can filter and modify the contents of both inbound and outbound requests.
The next and final chapter of the book is about hosting ASP.NET in existing Win32 processes. As we repeatedly mentioned, ASP.NET consists of an instance of the CLR hosted by a Win32 worker process. The great news is that the hosting process can be any Win32 process, including your own process. The capability of processing .aspx pages, therefore, is not an exclusive feature of IIS. We'll demonstrate this technique and its various uses in the next chapter.
Part I - Building an ASP.NET Page
Part II - Adding Data in an ASP.NET Site
Part III - ASP.NET Controls
Part IV - ASP.NET Application Essentials
Part V - Custom ASP.NET Controls
Part VI - Advanced Topics
Index