- Recognize the role of custom handlers in ASP.NET
- Write custom binary handlers
- Write just-in-time compiled custom handlers
- Let IIS and ASP.NET know about your custom handler
This tutorial covers writing custom HTTP handlers. In Tutorial 2 we saw the ASP.NET pipeline. Remember that the endpoint of all requests handled by ASP.NET is always an implementation of IHttpHandler.
After completing this tutorial, you will be able to
Recognize the role of custom handlers in ASP.NET
Write custom binary handlers
Write just-in-time compiled custom handlers
Let IIS and ASP.NET know about your custom handler
This tutorial covers writing custom HTTP handlers. In Tutorial 2 we saw the ASP.NET pipeline. Remember that the endpoint of all requests handled by ASP.NET is always an implementation of IHttpHandler.
ASP.NET includes several classes capable of handling requests in the most common ways. For example, the Page class handles requests by interpreting the query strings and returning meaningful UI-oriented HTML. The Service class interprets incoming query strings as method calls and processes them accordingly. So far, we've been focusing on a single handler¡ª[[<img src="images/shy.gif"/>]]System.Web.UI.Page. However, there are other times when it's appropriate to tweak the processing or even handle it in a completely different way. You may find yourself needing to handle a request in a way not already provided through the System.Web.UI.Page or the System.Web.Services.Service classes. What do you do then? ASP.NET supports custom HTTP handlers for just such occasions.
So far, we've focused most attention upon the Page class. The Page class is responsible primarily for managing the UI aspects of an application. Because UI processing is very involved (and much of it is boilerplate-type code) the Page class has a great deal of functionality built into it. The Page class will solve the majority of user interface requiring UI processing.
Although we haven't come across Web services yet, the WebService class implements the details required to interpret HTTP requests as method calls. Clients call Web services by packaging method calls in an XML format formalized as the Simple Object Access Protocol (SOAP). Clients call Web services in the same way they make HTTP requests for Web pages¡ªvia HTTP GET and POST requests. When the request reaches the server, it becomes the server's job to unpack the parameters, place them on a call stack, and finally invoke the correct method. Most of the work required to make a method call via HTTP is well-understood and consistent and may be pushed down into the Service class.
As we saw in Tutorial 2 on ASP.NET fundamentals, the endpoint for all HTTP requests destined for ASP.NET is a class implementing IHttpHandler. IHttpHandler is a simple interface, including a mere two methods. However, any class implementing that interface qualifies to participate in the HTTP pipeline as an HTTP handler. We'll see the interface in detail shortly.
HTTP handlers are simply classes implementing IHttpHandler (just as HTTP Modules are classes implementing IHttpModule). Handlers are listed inside Web.Config. As with the HTTP Modules, ASP.NET comes out of the box with several HTTP handlers already (for implementing features such as tracing and preventing access to sensitive files on the site). ASP.NET comes with these HTTP handlers already registered in the master Web.Config (which resides alongside Machine.Config in the main configuration directory) configuration file.
So far, ASPX, ASAX, and ASCX files have seemed to magically work within ASP.NET. For example, we saw earlier that simply surfing to an ASPX file causes ASP.NET to compile the file just-in-time and to synthesize a class based on System.Web.UI.Page. The reason the ASPX files work that way is because ASP.NET includes handlers for that functionality.
ASP.NET HTTP handlers are specified in Web.Config in much the same way as HTTP Modules. The format of the handler elements includes four items. First, they include a file name and/or extension to which the handler applies. This is done through the add attribute. Remember, all HTTP requests come to the server as resource requests¡ªthe HTTP protocol is built around the idea that requests contain file names. The second part of the handler specification, verb, is a list of verbs to which this handler applies. These verbs correspond to the HTTP specification. For example, you might want a handler to apply only to GET and not to POST requests. Or you may wish to have a handler apply to all requests. The third element, type, is the name of the .NET type assigned to handle the request. Finally, the last attribute, validate, specifies whether or not ASP.NET should load the class at startup immediately or wait until a matching request is received.
Listing 18-1 includes a smattering of the HTTP handlers already installed as part of ASP.NET's Web.Config file.
Listing 18-1
<httpHandlers>
<add path="trace.axd" verb="*"
type="System.Web.Handlers.TraceHandler" validate="True" />
<add path="WebResource.axd" verb="GET"
type="System.Web.Handlers.AssemblyResourceLoader" validate="True" />
<add path="*.axd" verb="*"
type="System.Web.HttpNotFoundHandler" validate="True" />
<add path="*.aspx" verb="*"
type="System.Web.UI.PageHandlerFactory" validate="True" />
<add path="*.ashx" verb="*"
type="System.Web.UI.SimpleHandlerFactory" validate="True" />
<add path="*.asax" verb="*"
type="System.Web.HttpForbiddenHandler" validate="True" />
<add path="".ascx" verb="*"
type="System.Web.HttpForbiddenHandler" validate="True" />
<add path="*.master" verb="*"
type="System.Web.HttpForbiddenHandler" validate="True" />
<add path="*.config" verb="*"
type="System.Web.HttpForbiddenHandler" validate="True" />
<add path="*.cs" verb="*"
type="System.Web.HttpForbiddenHandler" validate="True" />
<!¡ªMore handlers follow¡ -->
</httpHandlers>Let's take a look at a couple of specific handlers¡ªthe Trace handler and the Forbidden handler¡ªto get a good idea as to how having a separate request handling facility (i.e., one that is not tied specifically to UI or to Web services) can be useful.
One of the best examples of custom handling is the Trace handler that is built into ASP.NET. We looked at tracing in Tutorial 16. You turn tracing on within the Web.Config file by inserting the trace element <trace enabled=true />. This instructs the ASP.NET runtime to store summaries of the requests going through the site so they may be viewed for diagnostic purposes.
ASP.NET caches the tracing output in memory. To view the trace results, you surf to the virtual directory managing the site and ask for a specific file: Trace.axd. Take a look at Listing 18-1 and you'll see the first entry among all the standard HTTP handlers is for a resource named Trace.axd. The tracing functionality behind ASP.NET falls outside of normal UI processing, so it makes sense that tracing is handled by a custom handler.
When you surf to the Trace.axd resource, the handler renders HTML that looks like the output shown in Figure 18-1. The processing for this handler is very specific¡ªthe handler's job is to render the results of the last few requests. As you can see in Figure 18-2, selecting the View Details link resubmits the request with a parameter id=3 in the query string. This causes the handler to render the details of the third request. Figure 18-3 shows the IIS file mapping for files with the .axd extension. The ISAPI extension DLL handling the request is aspnet_isapi.dll. That means IIS will pass requests for files with an extension of .axd on to ASP.NET. Once inside the ASP.NET pipeline, the Web.Config file tells ASP to handle the request with the Trace handler.
Figure 18-1 The output of the Trace.axd handler.
Figure 18-2 The output of the Trace.axd handler when drilling down into a request summary.
Figure 18-3 IIS understands files with the extension .axd.
If you look through the default Web.Config file a bit more, you'll see some other handlers. Source code is banned explicitly from normal clients by default. Notice files such as *.cs, *.config, and *.vb are handled by the Forbidden handler. If you try to look at source code via a Web browser, ASP.NET returns the page shown in Figure 18-4 by default:
Figure 18-4 What happens when you try to view forbidden content.
Remember that ASP.NET's configuration is very malleable and that you may choose to let clients see your source code by one of two means. You may remove the source code extension to ASP.NET mappings within IIS. Alternatively, you may write your own source code viewer handlers and declare them in your application's Web.Config file.
These handlers plug into the pipeline by implementing IHttpHandler. Let's take a look at this key interface.
Here it is. Shield your eyes while you look at Listing 18-2.
Listing 18-2
public interface IHttpHandler
{
void ProcessRequest(HttpContext ctx);
bool IsReusable {get;}
}There's really not much to it, is there? The interface includes a method named ProcessRequest and a property named IsReusable. If the handler instance can be used multiple times, then IsReusable should return true. The heart of the handler is the ProcessRequest method that includes a single parameter: the current HttpContext.
Once a request finally arrives at the handler (through the ProcessRequest method), ProcessRequest can literally do anything to respond to the request. The Trace.axd handler responds to a GET request by listing the requests being tracked by the runtime. The forbidden handler responds by throwing up a roadblock so the client can't see the source. A custom Web service might respond to the request by parsing the XML payload, constructing a call stack, and making a call to an internal method.
The following example illustrates how to handle forms processing in a custom handler.
Implementing IHttpHandler is simple¡ªat least from the architectural point of view. The ProcessRequest method takes a single parameter¡ªthe current HttpContext. However, the code inside ProcessRequest is free to do just about anything possibly making the internal processing quite complex! The following example illustrates taking over the entire form-rendering process to display a list of choices within a combo box, allowing the end client to select from the choices, and finally rendering the chosen item.
Writing a Custom Handler
Create a project named CustomHandlers. Make it an HTTP site that uses IIS. This will be important because IIS will need to direct requests for a specific file extension to use aspnet_isapi.dll.
Add a new class library subproject to the CustomHandlers Web site (just as you did when you created an HTTP Module). Name the project CustomFormHandlerLib. The name of the class it generates for you is Class1. Rename the file CustomFormHandler.cs and the class CustomFormHandler.
The library generated by Visual Studio comes without any knowledge of the ASP.NET classes. Add a reference to the System.Web assembly.
To turn the CustomFormHandler class into an eligible handler, add the IHttpHandler interface to the inheritance list.
using System; using System.Collections.Generic; using System.Text; using System.Web; public class CustomFormHandler : IHttpHandler { public void ProcessRequest(HttpContext ctx) { ManageForm(ctx); } public void ManageForm(HttpContext context) { context.Response.Write("<html><body><form>"); context.Response.Write( "<h2>Hello there. What's cool about .NET?</h2>"); context.Response.Write( "<select name='Feature'>"); context.Response.Write( "<option> Strong typing</option>"); context.Response.Write( "<option> Managed code</option>"); context.Response.Write( "<option> Language agnosticism</option>"); context.Response.Write( "<option> Better security model</option>"); context.Response.Write( "<option> Threading and async delegates</option>"); context.Response.Write( "<option> XCOPY deployment</option>"); context.Response.Write( "<option> Reasonable HTTP handling framework</option>"); context.Response.Write("</select>"); context.Response.Write("</br>"); context.Response.Write( "<input type=submit name='Lookup' value='Lookup'></input>"); context.Response.Write("</br>"); if (context.Request.Params["Feature"] != null) { context.Response.Write("Hi, you picked: "); context.Response.Write( context.Request.Params["Feature"]); context.Response.Write( " as your favorite feature.</br>"); } context.Response.Write("</form></body></html>"); } public bool IsReusable { get { return true; } } }
The code within the ProcessRequest will render a <form> tag and a <selection> tag that renders a form that can be submitted on the browser. When the form is submitted back to the server, the parameters collection will have a Features element in it. The code examines the parameter collection to see if it references a feature, and displays the feature if it's been selected.
The class library you just created deposits its output in the project directory. In order for ASP.NET to use the page, the resulting executable needs to live in the virtual directory's \bin subdirectory. Right-click on the CustomHandler solution (in the Solution Explorer) and add a bin directory to the project. Then highlight the CustomFormHandlerLib project, select the project's build properties, and the compiled output to point to the bin directory. While you're in the configuration page, make sure the name of the target assembly is CustomFormHandlerLib.
Now update Web.Config so that it uses the handler when clients request the CustomFormHandler resource. If you don't already have a Web.Config in the project, add one. Then insert an httpHandlers section that points requests for the CustomFormHandler to the new CustomFormHandler class.
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <appSettings/> <connectionStrings/> <system.web> <httpHandlers> <add path="customformhandler" verb="*" type="CustomFormHandlerLib, CustomFormHandler" validate="True" /> </httpHandlers> </system.web> </configuration>You need to tell IIS about the new file types to be handled by the CustomFormHandler. Open IIS and drill down to the CustomHandler virtual directory. Right-click on the directory and select Properties. Then click the Configuration button to show the file types and their mappings.
Click the Add button and create a mapping between files with the extension *.cstm and aspnet_isapi.dll. You can use the Browse button to search for the aspnet_isapi.dll assembly in the Windows directory on your machine.
Finally, create a blank file named CustomHandler.cstm in the IIS virtual directory. The file with that extension needs to be there to work.
Surf to the customhandler.cstm file and ASP.NET will invoke the handler.
Of course, most of this processing could be handled more easily by setting up a Web form. However, this example shows the flexibility of the ASP.NET handler architecture. It should also give you more appreciation for the Web form and custom controls machinery within ASP.NET.
The CustomFormHandler Using VB
From the runtime's point of view, it doesn't matter how the handler was written¡ªit just needs to fully implement the IHttpHandler interface. Listing 18-3 illustrates the same handler written in VB.NET.
Listing 18-3
Imports System.Web
Public Class CustomFormHandlerVB : Implements System.Web.IHttpHandler
Public Sub ManageForm(ByVal context As HttpContext)
context.Response.Write("<html><body><form>")
context.Response.Write_
("<h2>Hello there. What's cool about .NET?</h2>")
context.Response.Write("<select name='Feature'>")
context.Response.Write_
("<option> Strong typing</option>")
context.Response.Write_
("<option> Managed code</option>")
context.Response.Write_
("<option> Language agnosticism</option>")
context.Response.Write_
("<option> Better security model</option>")
context.Response.Write_
("<option> Threading and async delegates</option>")
context.Response.Write_
("<option> XCOPY deployment</option>")
context.Response.Write_
("<option> Reasonable HTTP handling framework</option>")
context.Response.Write("</select>")
context.Response.Write("</br>")
context.Response.Write_
("<input type=submit name='Lookup'value='Lookup'></input>")
context.Response.Write("</br>")
If (context.Request.Params("Feature")) <> Nothing Then
context.Response.Write("Hi, you picked: ")
context.Response.Write(context.Request.Params("Feature"))
context.Response.Write(" as your favorite feature.</br>")
End If
context.Response.Write("</form></body></html>")
End Sub
Public ReadOnly Property IsReusable()_
As Boolean Implements System.Web.IHttpHandler.IsReusable
Get
Return True
End Get
End Property
Public Sub ProcessRequest_
(ByVal context As System.Web.HttpContext) Implements
System.Web.IHttpHandler.ProcessRequest
ManageForm(context)
End Sub
End ClassThe example accompanying this tutorial includes the previous handler named CustomFormHandlerVB in an assembly named CustomFormHandlerLibVB. Listing 18-4 shows how to refer to the second handler.
Listing 18-4
<httpHandlers>
<add path="customhandler.cstm" verb="*"
type="CustomFormHandler, CustomFormHandlerLib" validate="True"/>
<add path="customhandlerVB.cstm" verb="*"
type="CustomFormHandlerVB, CustomFormHandlerLibVB" validate="True"/>
</httpHandlers>In Tutorial 13, we looked at session state. Session state works automatically within the context of System.Web.UI.Page. However, handlers need to turn on the ability to use session state deliberately.
The .NET architecture uses an interesting idiom known as marker interfaces. Marker interfaces are empty interfaces (without any methods or properties defined). Their sole purpose is to signal the runtime as to various aspects of the application. For example, ASP.NET runtime often uses them to turn on and off various features. When the runtime detects a marker interface as part of an object's class hierarchy, the runtime can bring into play certain features.
For a handler to use session state, it must have the System.Web.SessionState.IRequiresSessionState interface in its inheritance list. That way the runtime will know to load and store session state at the beginning and end of each request.
Listing 18-5 shows a handler with session state enabled.
Listing 18-5
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.SessionState;
using System.Web;
public class HandlerWithSessionState :
IHttpHandler, IRequiresSessionState
{
public void ProcessRequest(HttpContext ctx)
{
String strData = (String)ctx.Session["SomeSessionData"];
if (strData == null)
{
ctx.Session["SomeSessionData"] = "This goes in session state";
}
ctx.Response.Write("This was in session state: " + strData);
}
public bool IsReusable {
get
{
return true;
}
}
}Just as ASPX files can be compiled on the fly (just-in-time), so can handlers. Generic handlers have an extension of ASHX. They're equivalent to custom handlers written in C Sharp or Visual Basic.NET in that they contain classes that fully implement IHttpHandler. They're convenient in the same way ASPX files are convenient. You simply surf to them and they're compiled automatically.
The following example illustrates the CustomFormHandler implemented as a ¡°generic handler.¡±
Writing a Generic Handler
Add a ¡°generic¡± handler to the Web site. Go to the Solution Explorer, right-click on the CustomHandler Web site node and select Add New Item. Select Generic Handler from the templates. Name the handler CustomFormHandler.ashx:
Visual Studio generates a handler that looks like this. It includes a stubbed-out Process[[<img src="images/shy.gif"/>]]Request method and IsReusable property.
<%@ WebHandler Language="C#" %> using System.Web; public class CustomFormHandler : IHttpHandler { public void ProcessRequest (HttpContext context) { context.Response.ContentType = "text/plain"; context.Response.Write("Hello World"); } public bool IsReusable { get { return false; } } }Borrow the code from the earlier example to implement the handler. Replace the stubbed-out method and property with real implementations.
<%@ WebHandler Language="C#" %> using System.Web; public class CustomFormHandler : IHttpHandler { public void ProcessRequest (HttpContext context) { ManageForm(context); } public void ManageForm(HttpContext context) { context.Response.Write("<html><body><form>"); context.Response.Write( "<h2>Hello there. What's cool about .NET?</h2>"); context.Response.Write("<select name='Feature'>"); context.Response.Write("<option> Strong typing</option>"); context.Response.Write("<option> Managed code</option>"); context.Response.Write("<option> Language agnosticism</option>"); context.Response.Write("<option> Better security model</option>"); context.Response.Write( "<option> Threading and async delegates</option>"); context.Response.Write("<option> XCOPY deployment</option>"); context.Response.Write( "<option> Reasonable HTTP handling framework</option>"); context.Response.Write("</select>"); context.Response.Write("</br>"); context.Response.Write( "<input type=submit name='Lookup' value='Lookup'></input>"); context.Response.Write("</br>"); if (context.Request.Params["Feature"] != null) { context.Response.Write("Hi, you picked: "); context.Response.Write(context.Request.Params["Feature"]); context.Response.Write(" as your favorite feature.</br>"); } context.Response.Write("</form></body></html>"); } public bool IsReusable { get { return false; } } }Browse to the CustomFormHandler.ashx file. It should work in just the same way as the handlers implemented in CustomFormHandler.cs and CustomFormHandler.VB, as shown in the following graphic:
The advantage of using the generic handler is twofold. First, it's usually much more convenient to generate a simple handler than it is to create a whole new assembly to handle the request. Second, you don't need to run interference with either Web.Config or with IIS. That is, Web.Config and IIS already understand what to do about files with the extension of .ashx. Installing ASP.NET places those when mapping into IIS.
However, ASHX files have the same limitations as ASPX and ASCX files in terms of their place in an ASP.NET project. Simple generic handlers go with the project. That is, for the handler to work, it must accompany the whole project. Alternatively, custom handlers deployed as separate assemblies may be deployed and shared among the enterprise as Global Assembly assemblies (that is, strongly named assemblies placed in the Global Assembly Cache).
ASP.NET includes a number of built-in classes to handle most kinds of requests. For example, ASP.NET includes UI handlers (System.Web.UI.Page and System.Web.UI.Control). ASP.NET also includes a Web service handler (System.Web.Services.Service). These classes will probably solve most of the requirements you might come across. However, for those fringe cases requiring custom handling, ASP.NET supports the custom handler.
The endpoint for requests coming through ASP.NET is always a class implementing IHttpHandler. IHttpHandler has very little surface area. You simply override the IsReusable property and the ProcessRequest method. ProcessRequest can pretty much do anything you want it to. The example in this tutorial included a handler that manages rendering a form and handling input.
For a custom handler assembly to work, it must be mapped to a file path or extension in the application's Web.Config file. The extension must also be mapped within the IIS metabase.
ASP.NET also supports handlers that may be compiled just-in-time. Simple handlers are easy to create and deploy because you don't need to modify the Web.Config file, nor do you need to modify the IIS metabase.
To | Do This |
Create a custom handler assembly | Create a new class implementing IHttpHandler Override the IsReusable property Override ProcessRequest |
Assign a file mapping to the handler in ASP.NET | Mention the handler in the <httpHandler> node of the application's Web.Config file |
Assign a file mapping to the handler in IIS | Right-click on the virtual directory Select Properties Click the Configure button Click the Add button Add a new extension and map it to aspnet_isapi.dll |
Create a simple handler | Select Web site | Add New Item Select Generic Handler from the templates Insert your own code for responding to the request |
discuss this topic to forum
