Using HTTP Modules and Handlers to Create Pluggable ASP.NET Components

 

Scott Mitchell, 4GuysFromRolla.com
Atif Aziz, Skybow AG

September 2004

Summary: In this article, Scott Mitchell and Atif Aziz show how you can use HTTP modules and handlers to add error logging to your ASP.NET applications. (22 printed pages)

Download the MSDNElmah.msi sample file.

Contents

Introduction
ELMAH: Error Logging Modules And Handlers
A Brief Overview of HTTP Handlers and Modules
Examining ELMAH's Architecture
Adding ELMAH to an ASP.NET Web Application
Conclusion
References
Related Books

Introduction

Have you ever worked on an ASP.NET application and designed some useful functionality or feature set that you wanted to be able to easily reuse in another ASP.NET application? ASP.NET offers different tools for componentizing different types of functionality. The two most common tools for reuse in ASP.NET are:

  • User Controls and custom, compiled server controls for user interface elements and functionality.
  • .NET class libraries for business logic and data access code.

Two ASP.NET reuse tools that don't get much attention are HTTP modules and handlers.

If you're not familiar with what HTTP handlers and modules are, don't worry. We'll talk more about them later on in this article. For now, just understand that HTTP modules are classes that can be configured to run in response to events that fire during the request for an ASP.NET resource. An HTTP handler is a class that is responsible for rendering a particular resource, or a particular type of resource. In fact, each time you add an ASP.NET Web page to your project, you are essentially writing a HTTP handler. This is because when the HTML portion of an ASP.NET Web page gets dynamically compiled at run time, it directly or indirectly inherits from System.Web.UI.Page, which happens to be a HTTP handler implementation. This is true irrespective of whether you take the inline or code-behind strategy.

As you know, an ASP.NET application usually consists of a set of Web pages, which are invoked when they are requested by an end-user's browser. As ASP.NET developers, most of the code we write is specific to a request for a particular Web page, such as code in a particular page's code-behind class to display database results based on some search query. There are times, though, that we need to write code that is orthogonal to a single Web page, code that applies to all pages in an application. For example, we might want to track the order with which each user moves around our Web site. To do this, we'd need to have each page log the time of the request and information identifying the user.

One way to provide such logging functionality would be to add code that recorded the germane data in a database in the Page_Load event handler for each Web page on the site. This approach, however, is hardly maintainable or reusable. Each time we add a new ASP.NET page in our site, we'd need to make sure that we included the appropriate logging code. If we wanted to add similar functionality to another site, we'd need to go through each page in that site and add the requisite code. Ideally, the logging functionality would be logically and physically separate from the functionality of each individual page, and adding it to another site would be as simple as dropping an assembly in the site's /bin directory.

Such reuse and maintainability is quite possible with HTTP modules and handlers. In this article, we are going to examine a set of HTTP modules and handlers that have been designed to make error logging a highly maintainable and reusable exercise. The goal of this article is to demonstrate how HTTP handlers and modules can be used as a very high-level form of componentization, enabling entire sets of functionalities to be developed, packaged, and deployed as a single unit and independent of Web applications. We'll achieve this goal in large part through an examination of an application that benefits from reuse and componentization through HTTP handlers and modules.

ELMAH: Error Logging Modules And Handlers

The Error Logging Modules And Handlers (ELMAH), which we'll be examining throughout this article, were written by coauthor Atif Aziz (http://www.raboof.com/) and demonstrate an easy means to add error logging capabilities to an ASP.NET Web application. ELMAH illustrates how HTTP modules and handlers can be used to provide a high degree of componentization for code that is orthogonal to the Web application (such as application-wide logging). ELMAH is a truly pluggable solution, meaning that it can be dynamically added to a running ASP.NET Web application without any need for recompilation or redeployment.

No matter how well written and tested a particular Web application may be, things will still go awry every now and then. It may not be your code at fault, it could be that the e-mail server is not responding, or some corruption of data causes a cryptographic failure. Regardless of the reason, when an exception occurs, especially on a live site, it is important that the details of the exception are recorded in order to assist with diagnosing the problem. ELMAH provides a mechanism for centralized error logging and notification. Whenever an exception occurs in an ASP.NET application that is not caught, ELMAH is notified and handles the exception as spelled out in the Web.config file. This may include recording the details of the exception to a database, sending an e-mail to an administrator, or both.

ELMAH isn't designed to respond gracefully to unhandled exceptions. It simply records the details of unhandled exceptions. Once ELMAH has been added to an ASP.NET Web application, any unhandled exceptions raised in this application are logged. ELMAH doesn't affect the end user's experience when an unhandled exception occurs. They'll still see the "Server Error" page, or, if you have custom errors configured to handle HTTP 500 errors, they'll be redirected to a page with a more user-friendly message. But behind the scenes, ELMAH will have detected that an unhandled exception occurred and recorded the details.

ELMAH discovers unhandled exceptions by way of the HttpApplication object's Error event. The Error event is raised whenever an uncaught exception bubbles up during request processing be that from a .NET class library or an ASP.NET Web page. Bear in mind that a lot of ASP.NET applications incorrectly implement custom error pages and handling by calling the Server.ClearError() method. Clearing the error will prevent the Error event from firing (as well as being reported to the client) and so ELMAH will never get a chance to log the exception. To put it another way, when using ClearError() in a custom error page, your user will see that a problem occurred, but you won't.

Note For more information on creating custom error pages, read Eli Robillard's article Rich Custom Error Handling with ASP.NET.

Note When an unhandled exception occurs in an ASP.NET Web service, the Error event is not bubbled up to HTTP modules and thus ELMAH. Rather, it is intercepted by the ASP.NET runtime and a SOAP fault is returned to the client. To have an error logged in a Web service, you would need to create a SOAP Extension that listened for SOAP faults.

In addition to recording details of unhandled exceptions, ELMAH also comes with a set of HTTP handlers for viewing the error log. There is a Web interface to the log, which can provide a list of all unhandled errors, as well as details about a particular error (see Figures 1 and 2).

Aa479332.elmah_fig01(en-us,MSDN.10).gif

Figure 1. Viewing the Error Log

Aa479332.elmah_fig02(en-us,MSDN.10).gif

Figure 2. Viewing an error

This error log can also be rendered as RSS, thereby allowing an administrator to receive notifications through her favorite RSS aggregator when an error has occurred (see Figure 3).

Aa479332.elmah_fig03(en-us,MSDN.10).gif

Figure 3. RSS feed of errors

Note RSS, which stands for Really Simple Syndication, is an XML-formatted standard that is commonly used to syndicate news and other types of changing content. To learn more about RSS, including how to syndicate content using RSS, as well as how to create a Web-based RSS reader, consider reading Creating an Online News Aggregator with ASP.NET.

For brevity, this article touches upon only a subset of the features of ELMAH, focusing on the key components. The complete code is available for download with this article and we encourage you to study it thoroughly to get at the implementation details. There is also a GotDotNet Workspace setup for ELMAH at http://workspaces.gotdotnet.com/elmah for the purpose of discussions, reporting issues, and staying up to date with any changes.

Existing Solutions for Centralized Error Logging

While ASP.NET does not provide built-in error logging and viewing capabilities, Microsoft's Patterns & Practices Group have created an open-source error logger—the Exception Management Application Block (EMAB). The EMAB was designed to work with both desktop and Web-based .NET applications, but one can't help feel that the EMAB was primarily designed for desktop applications with Web applications as an afterthought because the EMAB, by default, publishes exception details to the Windows Event Log. While the Event Log is a suitable backing store for concise exception information for a desktop application, most Web applications—especially those being hosted on a shared server at a Web hosting company—steer clear of the Event Log because using the Event Log requires special permissions to be established to allow the ASP.NET application to write to the Event Log. Granted, the EMAB is flexible enough that you can create a custom publisher that records information to a database, but that's an extra step that you, the developer, are tasked with.

Note ELMAH ships with a database-logging module for Microsoft SQL Server 2000, which we'll discuss later. With ELMAH you can also create custom exception loggers, such as a logger that records exception details to an XML file on the Web server's file system. In fact, you could extend ELMAH to use the Exception Management Application Block, if you already had a custom publisher written for the EMAB that you wanted to use.

How the EMAB is used to record exception information strongly influences the maintainability and reusability of the Web application. For example, a naïve approach to recording exception information would be to place a try ...catch block around each block of code in each ASP.NET Web page, calling the EMAB in the catch section.

private void Page_Load(object sender, EventArgs e)
{
  try {
    // Code that might cause an exception
  }
  catch (Exception ex) {
    // record exception information by calling exception logger library
  }
}

This approach is foolhardy since it tightly couples the exception logging to each and every ASP.NET Web page, making it anything but maintainable or reusable. A better approach would be to utilize the EMAB in the Application_Error event in Global.asax. This approach offers a more loosely coupled, maintainable, and reusable architecture, as the exception publishing code does not touch any ASP.NET Web page and is instead located in one, centralized location. The downside of this approach is that it is not pluggable. To add this error logging functionality to another ASP.NET Web application, you'd need to modify that application's Global.asax, thereby needed to recompile and redeploy the application.

The point of this article is not to introduce a replacement for the EMAB. Rather, it is to highlight the componentization made possible by HTTP handlers and modules. ELMAH illustrates how one can take a common task, such as centralized error logging, and componentize it to ease maintainability and afford a high degree of reusability. The purpose of ELMAH is to offer guidance for componentizing applicable functionality.

A Brief Overview of HTTP Handlers and Modules

Before we move on to examining the specifics of ELMAH's architecture and implementation, let's take a moment to review HTTP handlers and modules. Whenever a request arrives at an IIS Web server, IIS examines the extension of the request to decide how to proceed. For static content like HTML pages, CSS files, images, JavaScript files, and so on, IIS handles the request itself. For dynamic content like ASP pages, ASP.NET Web pages, and ASP.NET Web Services, IIS delegates the request to a specified ISAPI Extension. An ISAPI Extension is a piece of unmanaged code that knows how to render requests of a particular type. For example, the asp.dll ISAPI Extension is responsible for rendering requests for classic ASP Web pages; the aspnet_isapi.dll ISAPI Extension is invoked when a request comes in for an ASP.NET resource.

In addition to ISAPI Extensions, IIS also allows for ISAPI Filters. An ISAPI Filter is a piece of unmanaged code that can run in response to events raised by IIS. During the lifecycle of a request, IIS passes through a number of steps that raise corresponding events. For example, an event is raised when the request first reaches IIS, when the request is about to be authenticated, when the rendered content is about to be sent back to the client, and so forth. ISAPI Filters are commonly used to provide capabilities such as URL rewriting, compression, specialized authentication and authorization, specialized logging, and so on.

When a request for an ASP.NET resource reaches IIS, it is routed to the ASP.NET engine, which renders the content for the requested resource. The ASP.NET engine behaves a lot like IIS in that it raises a number of events as the request passes through the ASP.NET HTTP pipeline. Furthermore, the ASP.NET engine delegates rendering of the requested resource to a particular class. Whereas IIS uses unmanaged ISAPI Extensions and Filters, ASP.NET uses managed classes called HTTP handlers and modules.

An HTTP handler is a class that is responsible for rendering a particular type of resource. For example, the code-behind class for an ASP.NET Web page is an HTTP handler, knowing how to render the markup for the particular Web page. It helps to think about handlers as specialized renderers that know how to create the markup for a particular type of resource.

Note For a more in-depth discussion on HTTP handlers, along with some practical applications of handlers, be sure to read Serving Dynamic Content with HTTP Handlers.

An HTTP module is a class that can tap into the various events raised as a request passes through stages of its lifecycle on the server. One such ASP.NET application event is the Error event, which fires when an unhandled exception occurs, which is the event ELMAH is interested in.

Note For more information on HTTP modules, including a look at how to use HTTP modules to implement URL rewriting, check out URL Rewriting in ASP.NET.

Figure 4 provides a graphical representation of the ASP.NET HTTP pipeline. Note that the process starts with a request arriving at IIS. Assuming the requested resource is configured to be handled by the ASP.NET ISAPI Extension, IIS dispatches the request to the unmanaged aspnet_isapi.dll ISAPI Extension. This ISAPI Extension passes off the request to the managed ASP.NET engine. During the request lifecycle, one or more HTTP modules may execute, depending on what modules have been registered and what events they have subscribed to. Finally, the ASP.NET engine determines the HTTP handler that is responsible for rendering the content, invoking the handler and returning the generated content back to IIS, which returns it back to the requesting client.

Aa479332.elmah_fig04(en-us,MSDN.10).gif

Figure 4. Data flow through the Error Logger

ELMAH provides centralized error logging through an HTTP module that has an event handler for the Error event. When the event fires, ELMAH logs the exception details. ELMAH also uses HTTP handlers that are primarily responsible for generating HTML and RSS markup to display information from the error log.

Configuring an existing Web application to utilize various handlers or modules is accomplished by copying the module or handler assembly into the Web application's /bin directory and adding a few lines of configuration to the Web.config file.

To configure the HTTP modules for a Web application, include an <httpModules> section to the Web.config file that specifies the type of the module to add:

<httpModules>
   <add name="ModuleName" type="ModuleType" />
</httpModules>

The ModuleType is a string that spells out the module's type, which is the fully qualified class name (Namespace.ClassName) followed by the assembly name. The type attribute can also include versioning and culture information, along with a public key token that is required of strong-named assemblies. The following snippet shows the actual <httpModules> setting you'll need to use to include ELMAH's error logging Module in your ASP.NET application:

<httpModules>
  <add name="ErrorLog" type="GotDotNet.Elmah.ErrorLogModule, 
    GotDotNet.Elmah, Version=1.0.5527.0, Culture=neutral, 
    PublicKeyToken=978d5e1bd64b33e5" />
</httpModules>

An HTTP handler can be used in a Web application by adding an <httpHandlers> section to the Web.config file. Since an HTTP handler renders content for a particular type of resource, in addition to a type attribute the <httpHandlers> element contains a path attribute, which indicates what file paths or extensions should be mapped to this HTTP Handler. There's also a verb attribute that allows you to limit use of the handler to specific types of HTTP requests, as in a GET or POST request. The following example would create an HTTP Handler that is invoked for all requests to files with a .ashx extension.

<httpHandlers>
   <add verb="*" path="*.ashx" type="HandlerType" />
</ httpHandlers >

The type attribute for the HTTP handler is expressed using the same syntax options as with HTTP modules. These settings in the Web.config can also be placed in the machine.config file, which has the effect of enabling the handlers and modules for all Web applications on the server. The following snippet shows the <httpHandlers> element in the Web.config file in the demo included in this article's download. Note that it indicates that any incoming requests to /elmah/default.aspx should be rendered by the ErrorLogPageFactory class.

<httpHandlers>
    <add 
        verb="POST,GET,HEAD" 
        path="elmah/default.aspx" 
        type="GotDotNet.Elmah.ErrorLogPageFactory, 
          GotDotNet.Elmah, Version=1.0.5527.0, Culture=neutral, 
          PublicKeyToken=978d5e1bd64b33e5" />
</httpHandlers>

As you can see, adding HTTP modules and handlers to an ASP.NET Web application is very simple, can be done in a matter of seconds, and doesn't require any recompilation or redeployment of the ASP.NET application. This is why HTTP modules and handlers are a great tool for reuse, and afford a means to componentize your application into loosely-coupled, highly maintainable pieces.

Examining ELMAH's Architecture

ELMAH's architecture is comprised of three subsystems:

  • An error logging subsystem
  • An HTTP module subsystem
  • An HTTP handler subsystem

The error logging subsystem is responsible for two tasks: recording errors to the log and retrieving error information from the log. The HTTP module subsystem is responsible for logging an error when an unhandled exception occurs in the ASP.NET application. The HTTP handler subsystem provides a means for the error log to be rendered into markup, constituting a Web-based interface to the error log, as well as an RSS feed.

As Figure 5 shows, the HTTP module and handler subsystems both utilize the error logging subsystem. The HTTP module subsystem sends off exception information to the error logging subsystem, while the HTTP handler subsystem reads and renders the error information.

Aa479332.elmah_fig05(en-us,MSDN.10).gif

Figure 5. Where the Error Logging System fits

To better understand ELMAH's architecture, let's examine each of these three subsystems in more detail.

The Error Logging Subsystem

The error logging subsystem is responsible for recording errors in the log, as well as offering capabilities for retrieving details about a particular error, or a subset of the errors. This functionality is made available by a number of classes:

  • ErrorLog: This abstract class provides the contractual methods to both read from and write to the log.
  • Error: This class contains properties that describe the details of a particular error.
  • ErrorLogEntry: This class represents a particular Error instance for a particular ErrorLog. The ErrorLogEntry essentially groups an Error instance with the ErrorLog instance it originated from.

Let's take a look at these three classes and how they work with the HTTP modules and HTTP handler subsystems in order to provide a complete, centralized exception-logging utility.

Examining the ErrorLog Class

Depending on a particular project setup or strategy, you might want to employ a different backing store for the error log. For example, on a production server, you might want to log exceptions to Microsoft SQL Server, but on a development server you might be happy just storing the errors in a set of XML files or a Microsoft Access database. To offer the capability of using different backing stores, the error logging subsystem provides an abstract base class, ErrorLog, which defines the base methods that all ELMAH error loggers must implement. These methods are:

  • Log(Error): Logs an error to the backing store. The Error class represents information about an unhandled exception; we'll discuss this Error class in more detail shortly. In logging the error information, the Log() method must also assign a unique identifier to the error.
  • GetError(id): Returns information about a particular error in the log.
  • GetErrors(...): Returns a subset of errors from the log. This method is used by the HTTP handler subsystem to display the error log in a paged fashion, rather than displaying all errors at once.

ELMAH ships with two ErrorLog implementations:

  • SqlErrorLog: Records errors to a Microsoft SQL Server 2000 database using the System.Data.SqlClient provider. The SqlErrorLog requires SQL Server 2000 because it takes advantage of some of its XML features, but this is an implementation detail that can be changed.
  • MemoryErrorLog: Records errors in the application's memory (RAM). In other words, it is bound to the AppDomain such that each application receives its own private log. Needless to say, this log does not survive application restarts or lifetime, so it's mostly good for testing and temporary troubleshooting purposes when other implementations may fail.

You can use either of these exception loggers by simply adding a couple lines of text to your ASP.NET Web application's Web.config file. If you need to store error details to someplace other than SQL Server or application memory, you can create your own custom logger. To implement an error logger for ELMAH, create a class that extends ErrorLog and supply the implementation for the Log(), GetError(), and GetErrors() against your desired store.

Realize that both the HTTP module and handler subsystems in ELMAH interact directly with the specified ErrorLog class, be it SqlErrorLog, MemoryErrorLog, or your own custom log class. The HTTP module logs exception information by creating an Error instance and passing this to the ErrorLog method's Log() method. The HTTP handlers read details about one or more errors through the ErrorLog's GetError() and GetErrors() methods, which return either a specific ErrorLogEntry instance, or a set of ErrorLogEntry instances.

A Look at the Error Class

The Log() method of ErrorLog expects an input parameter of type Error. A custom Error class is used in place of the Exception class provided in the .NET Framework because the Exception class is more suited for communicating exception information across the code stack and during the lifetime of an application. However, Exception objects are not ideal for storing in an exception log because of storage, typing, and portability concerns. Yes, binary serialization could be utilized to store an Exception instance, but this would require that the Exception object be deserializable on a machine with the same set of types and assemblies available. This is an unacceptable limitation (especially from the point of view of administration and operations) because a log and its contents should be portable and not only be viewable on a machine with a particular runtime or configuration. Furthermore, an Exception instance often lacks periphery information specific to a Web application, such as the values of the current Web request's ServerVariables collection, something that can be invaluable for diagnosis. So, in short, the Error class acts as a surrogate for all exception types, holding over information from an exception raised in a Web application.

The complete list of Error properties are shown in Table 1.

Property Description
Exception The Exception instance represented by this error. This is a run-time property only that is never persisted along with an instance of the class.
ApplicationName The name of application in which this error occurred.
HostName The name of host machine where this error occurred. A good default is Environment.MachineName.
Type The type, class or category of the error. Usually this would be the full type name (sans the assembly qualification) of the exception.
Source The source of the error, usually the same as the Message property of an Exception object.
Message A brief text describing the error, usually the same as the Message property of an Exception object.
Detail Detailed text of the error, such as the complete stack trace.
User The User logged into the application at the time of the error, such as that returned by Thread.CurrentPrincipal.Identity.Name.
Time The date and time at which the error occurred. This is always in local time.
StatusCode The status code being returned in the response header as a result of the error. For example, this is 404 for a FileNotFoundException. Unfortunately, this value cannot always be reliably determined from within ASP.NET. For a few cases this StatusCode value may be reported as zero.
WebHostHtmlMessage The default HTML message that the Web host (ASP.NET) would have generated in absence of custom error pages.
ServerVariables A NameValueCollection of Web server variables, such as those contained in HttpRequest.ServerVariables.
QueryString A NameValueCollection of HTTP query string variables, such as those contained in HttpRequest.QueryString.
Form A NameValueCollection of form variables, such as those contained in HttpRequest.Form.
Cookies A NameValueCollection of cookies sent by the client, such as those contained in HttpRequest.Cookies.

The WebHostHtmlMessage property needs some explanation. If your ASP.NET Web application encounters an unhandled exception and you do not have your application configured to use custom error pages, you'll see a screen similar to the one shown in Figure 6. This is a screen every ASP.NET developer has seen far too many times.

Aa479332.elmah_fig06(en-us,MSDN.10).gif

Figure 6. Standard Error page

When an exception is raised, the actual HTML markup for the corresponding screen is accessed and saved in the WebHostHtmlMessage property of the Error class. When the page that shows detailed information about a particular exception is visited, if the corresponding Error instance has a value in its WebHostHtmlMessage property, the visitor is presented with a link to a page that will show the actual exception information screen (like that shown in Figure 6). The neat thing here is that you not only get the exception logged, but you can also visit the original error page generated by ASP.NET when examining the log later. And all this while you have custom errors enabled!

The Error class also has methods to serialize and deserialize its state to and from an XML format. See FromXml and ToXml in accompanying code for details.

The ErrorLogEntry Class: Associating an Error with an ErrorLog

The final class in the error logging subsystem is the ErrorLogEntry class, which associates an Error instance with an ErrorLog instance. When the HTTP handler subsystem calls the GetError() method to retrieve information about a particular exception, the GetError() method retrieves the information from the specific backing store and populates this information in an ErrorLogEntry instance. The ErrorLogEntry class contains three properties:

  • Id: The unique ID of the exception details.
  • Log: A reference to the ErrorLog instance that represents the backing store.
  • Error: A populated instance of the Error class with the details of the specific error.

While the GetError() method returns a single ErrorLogEntry instance, the GetErrors() returns a list of ErrorLogEntry instances. GetErrors() is especially designed to allow errors to be paged through n records at a time.

Figure 7 shows an updated view of ELMAH's architecture, showing greater detail in the error logging subsystem.

Aa479332.elmah_fig07(en-us,MSDN.10).gif

Figure 7. Updated architecture

The HTTP Module Subsystem

ELMAH consists of two HTTP modules: ErrorLogModule and ErrorMailModule. ErrorLogModule is an HTTP module that creates an event handler for the application's Error event. In the event of an unhandled exception, the HTTP module gets the appropriate error logger as specified in the application's configuration, and calls the Log() method on it, passing in an Error instance populated with the information of the exception and the HttpContext for the current request. The following source code shows the germane code from the ErrorLogModule class:

public class ErrorLogModule : IHttpModule
{
    public virtual void Init(HttpApplication application)
    {
        application.Error += new EventHandler(OnError);
    }

    protected virtual ErrorLog ErrorLog
    {
        get { return ErrorLog.Default; }
    }

    protected virtual void OnError(object sender, EventArgs args)
    {
        HttpApplication application = (HttpApplication) sender;
        LogException(application.Server.GetLastError(), 
          application.Context);
    }

    protected virtual void LogException(Exception e, 
      HttpContext context)
    {
        try
        {
            this.ErrorLog.Log(new Error(e, context));
        }
        catch (Exception localException)
        {
            Trace.WriteLine(localException);
        }
    }
}

The ErrorLogModule's execution begins in the Init() method, where it indicates to the ASP.NET runtime that the OnError() method should be invoked whenever the Error event is raised. The OnError() method references the HttpApplication object and calls the LogException() method, passing in the details of the last exception, as well as the HttpContext instance specific to the particular request. LogException() simply calls the appropriate ErrorLog class's Log() method, passing in a new Error instance. (The Error instance's constructor takes in an Exception and HttpContext instance, and populates the properties accordingly; refer to the source code available in the download for more information.)

The ErrorLogModule contains a read-only ErrorLog property, and returns the ErrorLog instance returned by ErrorLog.Default. Default is a static property of type ErrorLog in the ErrorLog class. It consults the Web application's configuration to determine what class to use for exception logging: SqlErrorLog, MemoryErrorLog, or a custom exception logging class.

Note In the section Adding ELMAH to an ASP.NET Web Application we'll examine how to configure a Web application to use a specific exception logger. It's as simple as adding a couple of lines to the Web.config or machine.config files.

The other HTTP module in the HTTP module subsystem is the ErrorMailModule class, which sends an e-mail to an administrator in the event of an exception. We won't be discussing this piece of ELMAH, although you can examine how to use this module in the code samples available in this article's download.

The HTTP Handler Subsystem

Recall that the purpose of HTTP handlers is to render the content for a particular type of resource. When a request comes into the ASP.NET HTTP pipeline, the ASP.NET engine examines the requested path and determines what HTTP handler should be used to handle the requested resource. Specifically, an ASP.NET application may be configured to have a particular path handled by either an HTTP handler or an HTTP handler factory. An HTTP handler factory is a class that is not directly responsible for rendering the content, but instead is responsible for selecting and returning an HTTP handler instance. This returned HTTP handler instance is then the one that is tasked with rendering the requested resource.

ELMAH's HTTP handler subsystem consists of a number of HTTP handler classes designed to produce markup to display the logged errors, along with a single HTTP handler factory class. The HTTP handler factory class, ErrorLogPageFactory, examines the PathInfo portion of the requested URL to determine what HTTP Handler should generate the output.

Note The PathInfo portion of a URL is any extra content following the file name, and is available through the Request object's PathInfo property. For example, in the URL http://www.example.com/someDir/somePage.aspx/somePath, somePath is the PathInfo portion of the URL. For more information on the terminology used for the various parts of a URL, and the corresponding Request object properties, refer to Rick Strahl's blog entry Making Sense of ASP.NET Paths.

The following code snippet shows the more interesting code from the ErrorLogPageFactory HTTP handler factory class.

public class ErrorLogPageFactory : IHttpHandlerFactory
{
    public virtual IHttpHandler GetHandler(HttpContext context, 
      string requestType, string url, string pathTranslated)
    {
        string resource = 
          context.Request.PathInfo.Length == 0 ? string.Empty :
            context.Request.PathInfo.Substring(1);

        switch (resource.ToLower(CultureInfo.InvariantCulture))
        {
            case "detail" :
                return new ErrorDetailPage();

            case "html" :
                return new ErrorHtmlPage();

            case "rss" :
                return new ErrorRssHandler();

            default :
                return new ErrorLogPage();
        }
    }
}

As you can see, the ErrorLogPageFactory class's GetHandler() method returns an HTTP handler instance based upon the PathInfo of the request. If the PathInfo is rss, an instance of the ErrorRssHandler HTTP handler is returned, which renders the log as an RSS feed. If the PathInfo is detail, an ErrorDetailPage HTTP handler instance is returned, which displays information about a particular exception.

In the ASP.NET Web application's settings, you must specify a path that maps to the ErrorLogPageFactory HTTP handler factory, such as ErrorLog.aspx. To view an RSS feed of the exception log, you could visit: http://www.example.com/ErrorLog.aspx/rss.

ELMAH's various HTTP handler classes—ErrorDetailPage, ErrorHtmlPage, ErrorRssHandler, ErrorLogPage, and so on—render different markup. The ErrorRssHandler HTTP handler, for instance, loops through the 15 latest errors and emits the proper XML markup to display this information in an RSS format. The other HTTP handlers are all derived, directly or indirectly, from the System.Web.UI.Page class (which is the class from which all ASP.NET code-behind classes are derived from). These page-related HTTP handlers override the Page class's Render() and OnLoad() methods to create an HTML interface displaying a pageable list of the logged exceptions. Refer back to Figures 1, 2, and 3 for screenshots of these pages.

Note While the Error class saves the ServerVariables, QueryString, Form, and Cookie collections, only the ServerVariables collection is displayed in the details for an exception. This is because the QueryString parameters and cookies are viewable through the ServerVariable's QUERY_STRING and HTTP_COOKIE parameters, respectively. The Form collection is omitted because this could include potentially tens of kilobytes of view state information that usually serves little purpose for most diagnosis. Of course, you could easily modify the details of the HTTP handler to include this information, if you so chose.

Now that we have examined ELMAH's three subsystems, let's take a look at how to add ELMAH to an existing ASP.NET Web application. Pay particular attention to how easy it is to add ELMAH to any site—a benefit of componentization that the HTTP handlers and modules afford.

Adding ELMAH to an ASP.NET Web Application

Adding ELMAH to an ASP.NET Web application is fairly simple, and is comprised of two steps:

  • Adding the ELMAH assembly to the Web application.
  • Configuring the Web application to use ELMAH's HTTP modules and HTTP handlers.

ELMAH can be applied to a particular Web application on a Web server by copying the assembly to the Web application's /bin directory, and configuring ELMAH's settings through the Web.config file. What's more, you can configure ELMAH to be applied to all Web applications on a Web server by adding the assembly to the Web server's Global Assembly Cache (GAC) and adding the same configuration settings in machine.config instead of Web.config.

In the Web.config (or machine.config) file, you'll need to add the following settings:

  • A <sectionGroup> element in the <configSections> element that defines a new section name, <gotdotnet.elmah>, with a section inside called <errorLog>, which has information about how to log exception information.
  • A <gotdotnet.elmah> section, with an inner section named <errorLog>, which contains a type reference to the exception logger you want ELMAH to use, along with any settings specific to that exception logger.
  • An entry in the <httpHandlers> section indicating the path that, when visited through a browser, will render various views on the error log.
  • An entry in the <httpModules> section that adds the ErrorLogModule to the ASP.NET HTTP pipeline.

The following snippet from the Web.config file included in this article's download illustrates how these four settings can be specified:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <!-- Allows for a new section group to the Web.config -->
    <sectionGroup name="gotdotnet.elmah">
      <!-- Indicates that inside the section group there will be an
              errorLog section -->
      <section name="errorLog" 
        type="System.Configuration.SingleTagSectionHandler, 
          System, Version=1.0.5000.0, Culture=neutral, 
          PublicKeyToken=b77a5c561934e089" />
    </sectionGroup>
  </configSections>

  <!-- This section group contains the type of the exception logger
         to use (SqlErrorLog, MemoryErrorLog, or a custom logger).
         It also contain properties pertinent to the exception logger
         (connectionString, for the SqlErrorLog). -->
  <gotdotnet.elmah>
    <errorLog type="GotDotNet.Elmah.SqlErrorLog, 
      GotDotNet.Elmah, Version=1.0.5527.0, Culture=neutral, 
      PublicKeyToken=978d5e1bd64b33e5" 
      connectionString="...connection string..." />
  </gotdotnet.elmah>

  <system.web>
    <!-- Register that a request to aspnetham/errorlog.aspx should
        be serviced by the ErrorLogPageFactory HTTP Handler factory -->
    <httpHandlers>
      <add verb="POST,GET,HEAD" path="elmah/default.aspx" 
        type="GotDotNet.Elmah.ErrorLogPageFactory, 
        Skybow.Samples.AspNetHam, Version=1.0.5527.0, 
        Culture=neutral, PublicKeyToken=978d5e1bd64b33e5" />
    </httpHandlers>

    <!-- Adds the ErrorLogModule HTTP Module to the HTTP pipeline. -->
    <httpModules>
      <add name="ErrorLog" type="GotDotNet.Elmah.ErrorLogModule, 
         GotDotNet.Elmah, Version=1.0.5527.0, Culture=neutral, 
         PublicKeyToken=978d5e1bd64b33e5" />
    </httpModules>
    
    ...
  </system.web>
</configuration>

The <sectionGroup> element in the <configSections> element spells out that there will be an additional section group in the configuration file called <gotdotnet.elmah>. Furthermore, it indicates that inside this custom section, there will be an <errorLog> section. Inside the actual <gotdotnet.elmah> element there is an <errorLog> element that specifies which error log implementation should be used. Recall that ELMAH ships with two built-in implementations, namely SqlErrorLog and MemoryErrorLog. You can specify which of these two to use, or you can specify to use a custom exception logger you may have created, in the <errorLog> element. The <errorLog> element also holds the settings specific to an error log implementation. For example, when using the <errorLog> element to indicate that the SqlErrorLog should be used, a connectionString property must be included that tells it how to connect to the database. The SQL script to create the appropriate table and associated stored procedures is included in the download.

Note If you want an administrator to be e-mailed in the event of an unhandled exception, you would need to add another <section> element in <sectionGroup> that defines a new element called <errorMail>. Furthermore, in the actual <gotdotnet.elmah> element, you'd need to add an <errorMail> element. Consult the Web.config file in the download for a sample of this syntax.

The <httpHandlers> section specifies that the ErrorLogPageFactory (an HTTP Handler factory) should be used to retrieve the HTTP handler that renders the content to view the error log. The value of the path attribute indicates the URL relative to the application's virtual root for getting at the error log display. You can change this to whatever you like, but make sure that it is a URL with an extension that is handled by the ASP.NET engine. That is, if you change the path to something like errors.log, you'll need to configure IIS to map requests to errors.log to the ASP.NET ISAPI Extension (aspnet_isapi.dll). If you want to ensure that only administrators can view the log, use ASP.NET's URL authorization capabilities to restrict access to a specific user or a set of users or roles. On the other hand, if you want to entirely disable Web-based access to the log then you simply don't configure the <httpHandlers> section.

The <httpModules> section adds the ErrorLogModule HTTP module to the ASP.NET HTTP pipeline. Make sure you include this <httpModules> setting, otherwise ELMAH won't be listening for the Error event, and therefore won't be logging any unhandled exceptions.

As you can see, adding ELMAH to an existing ASP.NET Web application is fairly straightforward. The simple deployment and reusability of ELMAH is due to the fact that it is componentized using HTTP modules and handlers.

Conclusion

Hopefully this article has been able to shed some light on how HTTP handlers and modules are great tools for componentizing functionality orthogonal to an ASP.NET Web application. Common tasks such as centralized, application-wide logging, or monitoring requests across the entire application, can be componentized through handlers and modules. By wrapping up this functionality into a set of components, you get reusability, maintainability, and deployment benefits without requiring any migration, integration or re-compilation of existing code and applications.

To demonstrate the componentization possible with HTTP modules and handlers, we examined ELMAH, a centralized error logging and mailing application. ELMAH uses an HTTP module to listen for any application-wide Error events, which is fired as a result of an unhandled exception bubbling up. Upon learning of an unhandled exception, ELMAH logs the exception to a SQL Server database, to memory, or, perhaps, to some other backing store. ELMAH can also e-mail the contents of the exception to one or more recipients like developers and operations staff.

In addition to an HTTP module, ELMAH contains a set of HTTP handlers and an HTTP handler factory to facilitate viewing of the error log through a Web-based medium. This includes not only a traditional Web page, but also an RSS feed. ELMAH maintains a discrete component by having the display functionality wrapped in an HTTP handler, as opposed to requiring that the Web application include an ASP.NET Web page that displays such information. Through the use of HTTP handlers, deploying ELMAH is a simple process, and does not require a recompilation of the Web application, or uploading an ASP.NET Web page to the production server.

ELMAH is only one example of the power of componentization that HTTP handlers and modules afford. Perhaps there are other application-wide processes that you have implemented that can benefit from being componentized with handlers and modules.

Happy Programming!

Acknowledgements

Before submitting this article to the MSDN editor, we had a number of volunteers help proofread the article and provide feedback on the article's content, grammar, and direction. Primary contributors to the review process for this article include Milan Negovan, Carl Lambrecht, Dominique Kuster, Roman Mathis, Raffael Zaghet, Muhammad Abubakar, and Patrick Schuler.

References

Atif Aziz has nearly 13 years of experience developing solutions on the Microsoft platform. He is a Principal Consultant at Skybow AG where his primary focus is to help customers understand and build solutions on the .NET development platform. Atif contributes regularly to the Microsoft developer community by giving talks at Microsoft and non-Microsoft conferences and writing articles for technical publications. He is an INETA speaker and president of the largest Swiss .NET User Group (dotMUGS). He can be reached at atif.aziz@skybow.com or via his Web site at http://www.raboof.com.

Scott Mitchell, author of five ASP/ASP.NET books and founder of 4GuysFromRolla.com, has been working with Microsoft Web technologies since 1998. Scott works as an independent consultant, trainer, and writer. He can be reached at mitchell@4guysfromrolla.com or via his blog, which can be found at http://ScottOnWriting.NET.

© Microsoft Corporation. All rights reserved.