asp:cover story
LANGUAGES: VB .NET | C#
TECHNOLOGIES: HTTP Modules | ASP.NET Configuration | .NET Event Handling | File I/O
Extend ASP.NET With HTTP Modules
Add power above and beyond traditional ASP.NET apps by inserting new functionality to request handlers.
By Brian Noyes
ASP.NET is a powerful environment for developing and running Web applications. But sometimes you need to insert functionality in the middle of that environment in ways that simply are not possible with code on or behind an ASP.NET page. You might need to perform processing at the earliest stages in the HTTP processing pipeline, before the request ever reaches any pages, or at the very end, after the response has been generated. Or you might need to intercept processing at the moment certain events occur, such as when the user is being authenticated. In the days before ASP.NET, the only real way to do this was through Internet Server Application Program Interface (ISAPI), which was fairly painful to program, and you needed a lot of expertise in C++, DLL programming, and Internet Information Server (IIS). Fortunately, as it has for many Web-development tasks, ASP.NET has made this kind of thing much easier. The ASP.NET architecture gives you the opportunity to add functionality at several different levels in the HTTP processing pipeline.
In this article, I'll discuss where those extension points are. I'll briefly cover how the processing pipeline works in ASP.NET and which classes are involved for which parts of the processing. Then I'll go into a little more detail about the best way to add functionality in the processing pipeline: creating HTTP modules.
This article's sample application uses a module to keep track, in a log file, of a user and each page he or she visits on a site. You could do this at the page level, but it is much easier by inserting an HTTP module in the pipeline to do the work. That way, you can add this functionality to any site simply by changing the web.config file and deploying an assembly to the Web application's bin directory. The code shown in this article is in VB .NET, but the download code contains the same thing in C#. Finally, I'll mention some other examples of what you could do with an HTTP module.
Get Tubed in the HTTP Pipeline
Ultimately, any processing that occurs in your Web application starts with an HTTP request and ends with an HTTP response. If your Web application maintains state (through cookies, session state, or some other means), any number of successive round trips of requests and responses could make up the processing of a single user experience on your site. But IIS usually hides these details and only lets you worry about implementing Web pages and their associated code to do what you need to do when a request comes in for a particular page.
When a request comes in for an address, IIS needs to locate an associated Web application for the request and load that application if it is not running already. Then, IIS takes the request variables (QueryString and Post parameters) located in the request headers and body and passes them along to the application or page to process. In classic ASP, IIS handled all this internally. IIS created an application instance on the first call to the server for a resource in a virtual directory, and the application passed on calls to its individual pages. If you wanted to intercept the calls anywhere in that process, you had to implement an ISAPI extension that implemented an application, or you had to implement an ISAPI filter that could do some processing on the responses as they came in and on the requests as they went out, basically sitting between IIS and the application (see Figure 1).
Figure 1. In traditional ASP development, your options were limited
either to handling things in your page or implementing ISAPI filters or
extensions to override the normal processing pipeline.
ASP.NET runs as an ISAPI extension, so any ASP.NET application is handled by the ASP.NET runtime instead of IIS. ASP.NET manages a pool of HttpApplication instances for each application and uses one of the instances from the pool for each request that comes in. These instances are based on the class declared in your global.aspx file or on the base HttpApplication class if no global.aspx file is present. Unlike classic ASP, ASP.NET also runs these instances in their own process, so if one app crashes, it doesn't bring down the whole server.
The next layer in the ASP.NET pipeline is made of HTTP modules, which are simply .NET classes that implement the IHttpModule interface and are configured for the application through the web.config file. ASP.NET also provides many HTTP modules configured in the machine.config file for such things as authentication and session-state services. At the end of the chain are HTTP handlers, which are simply classes that implement the IHttpHandler interface. The System.Web.UI.Page class that Web forms are derived from implements the IHttpHandler interface for you, so your typical HTTP handler is simply a page in your application (see Figure 2). Basically, you can add or modify functionality in an ASP.NET application at several levels: the application, module, and handler levels.
Figure 2. ASP.NET gives you several points at which you can intercept
the request handling: the application, module, and handler levels.
In extending ASP.NET, it's important to understand the lifecycle of a request and the events the HttpApplication raises during that lifecycle. You can wire event handlers in your HTTP module to react to the lifecycle events that interest you. You also need to keep in mind that the state of the HttpApplication instance changes throughout the lifecycle. For example, if you try to access the User object in the BeginRequest handler, the User object will be null. If you try to access the Session object after the ReleaseRequestState event, the Session object will be null. Figure 3 shows the events available.
Event Name |
Description |
|
BeginRequest |
Occurs for every request. This is the first chance to hook into a request. |
|
AuthenticateRequest |
Occurs when authentication is attempted. This is your chance to provide custom authentication. |
|
AuthorizeRequest |
Occurs when the requested resource is checked for access rights. |
|
ResolveRequestCache |
Used to handle caching of output responses (such as cached controls and pages). |
|
AcquireRequestState |
Obtains a state object for the request. |
|
PreRequestHandlerExecute |
Occurs just prior to control being passed to the HTTP handler (page). |
|
PostRequestHandlerExecute |
Happens just after the HTTP handler (page) has completed processing and has returned a response. |
|
ReleaseRequestState |
This is where the request's state goes out of scope. |
|
UpdateRequestCache |
Cache can be updated here if content has changed. |
|
EndRequest |
The last chance to do something with the request or response. |
|
Error |
Raised if some error occurs in the processing pipeline. |
|
PreSendRequestHeaders |
Signals that headers are about to be sent to the client. This is your chance to modify those headers. |
|
PreSendRequestContent |
Signals that the content of the response is about to be sent to the client. This is a good place to make modifications to content. |
|
Figure 3. This is the HttpApplication's lifecycle. As an HTTP request goes through the processing lifecycle, events are raised at key milestones to let you perform custom processing. The available events are shown in order. The last three events occur non-deterministically any time throughout the processing lifecycle.
Insert Yourself
HTTP modules provide the perfect insertion point for adding functionality or overriding behavior in Web applications because they reside between the application and the handler for every request that comes through. All you need to do to create one is create a class that implements the IHttpModule interface. This interface defines two methods: Init and Dispose. Init is called when your module is loaded and it's where you can wire up handlers for the HttpApplication events you're interested in. The Dispose method is called when the module is being unloaded. This method provides an opportunity to clean up any resources you might have held open. Then, you simply need to implement event handlers for the events you are interested in.
If you want your module to be used for all applications on
the machine, add it to the machine.config file. If you want it to apply to one
Web application only, add it to the web.config file for that app. The
machine.config file contains
type="MonitoredSite.UserTracker, MonitoredSite"/> You use an type="MyModules.MyFormsAuthenticationModule, Module1"/> Now I'll take you through building an HTTP module that
tracks user activity on a site. This article's downloadable code includes an
ASP.NET Web application project named MonitoredSiteVB. In it is an HTTP module
class named UserTracker. The module creates a log of user activity on the site,
including the time, session ID, username, and pages visited. The module also
tracks whether the user encountered any errors. I kept the logging
implementation simple in this sample, but you could implement a more scalable approach
in a similar fashion to write the user activity to a database or other store.
For a production module, you probably would want to implement it in a separate
class-library assembly project so it could be reused easily for other sites. The first step in building the module is to create the
module's class. This is a simple class that implements the IHttpModule
interface and its two methods, Init and Dispose. Then, you need to decide which
HttpApplication events you want to subscribe to. In this application, I
subscribed to the PostRequestHandlerExecute and Error events and added handlers
for them. I chose the PostRequestHandlerExecute event because I wanted to log a
session ID as well as the user and page they requested. If I had tried to do
this in the EndRequest event, the session would be out of scope already and
thus unavailable. Each HttpApplication event can be handled by an instance of
the EventHandler class, and the handlers are all passed the same parameters: an
object that is a reference to the current HttpApplication instance servicing
this request, and an EventArgs object that is empty. After creating an ASP.NET project in Visual Studio and
adding some simple pages to demonstrate navigation on the site, I added the
class in Figure 4 to the project as a new item. Imports System.Web Imports System.IO Imports System.Xml Public Class UserTracker Implements IHttpModule Public Sub Init(ByVal app As HttpApplication) _ Implements
IHttpModule.Init AddHandler
app.PostRequestHandlerExecute, _ AddressOf
Me.OnPostRequestHandlerExecute AddHandler
app.Error, AddressOf Me.OnError End Sub Public Sub Dispose() Implements IHttpModule.Dispose ' Nothing needed
here End Sub Public Sub OnPostRequestHandlerExecute(ByVal appObj As _ Object, ByVal eArgs As
EventArgs) ' Details omitted End Sub Public Sub OnError(ByVal appObj As Object, ByVal eArgs _ As EventArgs) ' Details omitted End Sub End Class Figure 4. The UserTracker HTTP module class provides
the extension of the HTTP processing pipeline for any applications that include
it in the HttpModules section of their web.config file. The class implements
the IHttpModule interface, which allows it to plug in to any ASP.NET Web
application. Figure 5 details how to wire up the events and handle
them. Basically, each handler begins by casting the appObj parameter to an
HttpApplication, then uses that object to access the Request.Url, User, and
Session properties of the application object. Then, the handler writes out the
date and time, session ID, username, and URL to a log file. Public Sub Init(ByVal app As
HttpApplication) _ Implements
IHttpModule.Init ' Add handlers for the
events we want AddHandler
app.PostRequestHandlerExecute, _ AddressOf
Me.OnPostRequestHandlerExecute AddHandler
app.Error, AddressOf Me.OnError fname =
"C:\logfile.txt" End Sub Public Sub
OnPostRequestHandlerExecute( _ ByVal appObj As
Object, _ ByVal eArgs As
EventArgs) Try Dim app As
HttpApplication Dim dttm As String Dim sesid As String Dim username As String Dim url As String Dim fname As String ' Get the appObj as a typed HttpApplication instance app = CType(appObj,
HttpApplication) ' Get the current Date
and Time dttm =
DateTime.Now.ToString ' Get the session ID sesid =
app.Session.SessionID ' Get the username username =
app.User.Identity.Name ' Get the URL
requested url =
app.Request.Url.ToString ' Write to the logfile fname =
HttpContext.Current.Server.MapPath( _ "./MonitoredSiteVB.log") Dim sw As New
StreamWriter(fname, True) sw.WriteLine(dttm +
", " + sesid + + ", "
+ username + ", " + url) sw.Close() Catch e As Exception ' Do nothing End Try End Sub Figure 5. Connect and handle the ASP.NET application
events in your HTTP module. You can wire up event handlers in the Init method
called when the module loads, then you can perform any custom processing you
want to insert into the HTTP request-processing pipeline in your event
handlers. The next thing you need to do to ensure your HTTP module
is called in the processing of the application is to add it to the web.config
file as I showed you before. For the VB project, this is how it looks: type="MonitoredSiteVB.UserTracker, MonitoredSiteVB"/> Because the app writes out a log file to the Web
application directory using the ASPNET account through which ASP.NET runs, you
also need to grant that account write permission to the application folder,
then add this code to your web.config file to ensure people cannot view your
logs: type="System.Web.HttpForbiddenHandler" /> Now you should be ready to run. Your module will be loaded
when the application starts up and will intercept the events for each request
when it comes in. Keep in mind that if the user clicks on the Back button in
the browser (or presses the backspace key), a request will not be issued, so
you won't see a log entry. Note also that embedded images on the page are
associated with the same request as the page itself, so they will not result in
a separate request even if they are configured to be handled by ASP.NET instead
of IIS directly. What else could you use HTTP modules for? Obviously a lot
more than I could mention or even imagine. But some common scenarios would be
to implement your own authentication service, pre-process destination URLs to
redirect or perform some custom processing on each request, or modify the
return response headers, such as to mask actual paths on your server with
aliased paths. You could process outbound responses to screen out "dirty" words
or proprietary phrases that might creep into documents published to the Web or
that come from an HTML-based discussion forum. Or maybe you want to implement
your very own Web-based protocol that does something similar to SOAP. The
possibilities are endless, but the beauty of it is how easy HTTP modules are to
write and configure as you have seen. HTTP modules are an easy way to add or modify the behavior
of a site without modifying the code for the pages of the site. You also can
handle events at the application level through your global.aspx file, or at the
handler level with classes or pages that implement the IHttpHandler interface.
Whichever way you go, ASP.NET provides you with much more flexibility for
customizing the behavior of your Web application than was possible with ASP -
and in a way that is a great deal easier to implement than ISAPI filters or
extensions. The sample code in this
article is available for download. Brian Noyes is an independent software consultant and
president of Software Insight (http://www.softinsight.com).
He's a Microsoft Certified Solution Developer with more than 11 years of
programming, design, and engineering experience. Brian specializes in
architecture, design, and coding of cutting-edge software systems, and he is a
contributing editor for asp.netPRO and other publications. E-mail him at mailto:[email protected]. Tell us what you think! Please send any comments about
this article to [email protected].
Please include the article title and author.