IIS Documentation


Introduction

When request come from client to the server a lot of operation is performed before sending response to the client. This is all about how IIS Process the request.  Here I am not going to describe the Page Life Cycle and there events, this article is all about the operation of IIS Level.  Before we start with the actual details, let’s start from the beginning so that each and everyone understand it’s details easily.  Please provide your valuable feedback and suggestion to improve this article.

What is Web Server ?

When we run our ASP.NET Web Application from visual studio IDE, VS Integrated ASP.NET Engine is responsible to execute all kind of asp.net requests and responses.  The process name is “WebDev.WebServer.Exe” which actually take care of all request and response of an web application which is running from Visual Studio IDE.

Now, the name “Web Server” come into picture when we want to host the application on a centralized location and wanted to access from many locations. Web server is responsible for handle all the requests that are coming from clients, process them and provide the responses.

What is IIS ?

IIS (Internet Information Server) is one of the most powerful web servers from Microsoft that is used to host your ASP.NET Web application. IIS has it’s own ASP.NET Process Engine  to handle the ASP.NET request. So, when a request comes from client to server, IIS takes that request and  process it and send response back to clients.

Request Processing :

Hope, till now it’s clear to you that what is Web server and IIS is and what is the use of them. Now let’s have a look how they do things internally. Before we move ahead, you have to know about two main concepts

1.    Worker Process
2.    Application Pool
Worker Process:  Worker Process (w3wp.exe) runs the ASP.Net application in IIS. This process is responsible to manage all the request and response that are coming from client system.  All the ASP.Net functionality runs under the scope of worker process.  When a request comes to the server from a client worker process is responsible to generate the request and response. In a single word we can say worker process is the heart of ASP.NET Web Application which runs on IIS.

Application Pool:  Application pool is the container of worker process.  Application pools is used to separate sets of IIS worker processes that share the same configuration.  Application pools enables a better security, reliability, and availability for any web application.  The worker process serves as the process boundary that separates each application pool so that when one worker process or application is having an issue or recycles, other applications or worker processes are not affected. This makes sure that a particular web application doesn’t not impact other web application as they they are configured into different application pools.

Application Pool with multiple worker process is called “Web Garden”.

Now, I have covered all the basic stuff like Web server, Application Pool, Worker process. Now let’s have look how IIS process the request when a new request comes up from client.

If we look into the IIS 6.0 Architecture, we can divided them into Two Layer
1.    Kernel Mode
2.    User Mode

Now, Kernel mode is introduced with IIS 6.0, which contains the HTTP.SYS.  So whenever a request comes from Client to Server, it will hit HTTP.SYS First.

Now, HTTP.SYS is Responsible for pass the request to particular Application pool. Now here is one question, How HTTP.SYS comes to know where to send the request?  This is not a random pickup. Whenever we creates a new Application Pool, the ID of the Application Pool is being generated and it’s registered with the HTTP.SYS. So whenever HTTP.SYS Received the request from any web application, it checks for the Application Pool and based on the application pool it send the request.

So, this was the first steps of IIS Request Processing.

Till now, Client Requested for some information and request came to the Kernel level of IIS means at HTTP.SYS. HTTP.SYS has been identified the name of the application pool where to send. Now, let’s see how this request moves from HTTP.SYS to Application Pool.

In User Level of IIS, we have Web Admin Services (WAS) which takes the request from HTTP.SYS and pass it to the respective application pool.

When Application pool receive the request, it simply pass the request to worker process (w3wp.exe) . The worker process “w3wp.exe” looks up the URL of the request in order to load the correct ISAPI extension. ISAPI extensions are the IIS way to handle requests for different resources. Once ASP.NET is installed, it installs its own ISAPI extension (aspnet_isapi.dll) and adds the mapping into IIS.

Note : Sometimes if we install IIS after installing asp.net, we need to register the extension with IIS using aspnet_regiis command.

When Worker process loads the aspnet_isapi.dll, it start an HTTPRuntime, which is the entry point of an application. HTTPRuntime is a class which calls the ProcessRequest method to start Processing.

When this methods called, a new instance of HTTPContext is been created.  Which is accessible using HTTPContext.Current  Properties. This object still remains alive during life time of object request.  Using HttpContext.Current we can access some other objects like Request, Response, Session etc.

After that HttpRuntime load an HttpApplication object with the help of  HttpApplicationFactory class.. Each and every request should pass through the corresponding HTTPModule to reach to HTTPHandler, this list of module are configured by the HTTPApplication.

Now, the concept comes called “HTTPPipeline”. It is called a pipeline because it contains a set of HttpModules ( For Both Web.config and Machine.config level) that intercept the request on its way to the HttpHandler. HTTPModules are classes that have access to the incoming request. We can also create our own HTTPModule if we need to handle anything during upcoming request and response.

HTTP Handlers are the endpoints in the HTTP pipeline. All request that are passing through the HTTPModule should reached to    HTTPHandler.  Then  HTTP Handler  generates the output for the requested resource. So, when we requesting for any aspx web pages,   it returns the corresponding HTML output.

All the request now passes from  httpModule to  respective HTTPHandler then method and the ASP.NET Page life cycle starts.  This ends the IIS Request processing and start the ASP.NET Page Lifecycle.

Conclusion

When client request for some information from a web server, request first reaches to HTTP.SYS of IIS. HTTP.SYS then send the request to respective  Application Pool. Application Pool then forward the request to worker process to load the ISAPI Extension which will create an HTTPRuntime Object to Process the request via HTTPModule and HTTPHanlder. After that the ASP.NET Page LifeCycle events starts.

This was just overview of IIS Request Processing to let Beginner’s know how the request get processed in backend.  If you want to learn in details please check the link for Reference and further Study section.

Rich Custom Error Handling with ASP.NET


Introduction

The quality of a site should be measured not only by how well it works, but by how gracefully it fails. While developers need detailed error reports while debugging, visitors to the site should be shielded from these. Technical errata only serve to confuse, disappoint, and reveal cracks in the armor.

If an error page is displayed, it should serve both developers and end-users without sacrificing aesthetics. An ideal error page maintains the look and feel of the site, offers the ability to provide detailed errors to internal developers—identified by IP address—and at the same time offers no detail to end users. Instead, it gets them back to what they were seeking—easily and without confusion. The site administrator should be able to review errors encountered either by e-mail or in the server logs, and optionally be able to receive feedback from users who run into trouble. Is this the stuff of dreams? No more.

There are several obstacles standing between the current built-in features of ASP.NET 1.1 and realizing the ideal. For one, the built-in detailed error page is fixed and cannot be customized. Further, a custom error page (as set in web.config) does not have access to the last error thrown, so it is really only useful to make the apology prettier. While thecustomErrors tag in web.config has a property to provide the custom page to external users and the detailed error page only to the local user (mode=RemoteOnly), how many developers debug at the server console?

All these problems can be solved, but first it helps to understand the basics. Peter Bromberg wrote two resources on exception handling that everyone should read as a prerequisite for this article: Documenting Exceptional Developers and Build a Really Useful ASP.NET Exception Engine. In addition, source code is provided with this article (click the link near the top of this page) for a complete rich custom error system. As with my previous MSDN article, the sample is from my favorite fake Swedish journal of finance: Fjorbes Online. All these online resources will introduce you to the basics. Now let’s see how to put it all together to bring error handling from the dark ages to the space age.

Errors Raise Exceptions

When errors happen, an exception is raised or thrown. There are three layers at which you may trap and deal with an exception: in a try…catch…finally block, at the Page level, or at the Application level. The first two happen right inside a page’s code, and code for application events is kept inside global.asax.

The Exception object contains information about the error, and as the event bubbles up through the layers, it is wrapped in further detail. In rough terms, the Application_Errorexception contains the Page_Error exception, which expands on the base Exception, which triggered the bubbling in the first place.

The Exception Class

Not surprisingly, the call to get the last error is Server.GetLastError() and it returns an object of type Exception:

   Dim err As Exception = Server.GetLastError()

You will find an Exception class reference here. Some of the properties and methods of the Exception class are more useful than others. (See Table 1.)

Table 1. Exception class properties and methods rated for usefulness

Property / Method() Return Type Description
Message String The error message. Useful. Available with debugging on or off.
Source String The application or object that caused the error. Not of much use when an .aspx file fails, as .NET generates a random name when it compiles an .aspx into IL (for example, “MyPage.aspx” can become “ecpgatxa”). Reasonably useful for debugging classes and controls.
StackTrace String When execution ends, the stack is unwound. This means that every call from the original page request, down to the line that triggered the error, is popped off the execution stack and noted. Happily, even line numbers are recorded in the base Exception when debugging is turned on. Only the method name is reported when debugging is off. This is highly useful.
TargetSite MethodBase The method that threw the Exception. This also shows up in the StackTrace. It is not as useful on its own.
HelpLink String Can hold a URL that might help the user, but usually doesn’t. Consider implementing it when throwing custom exceptions.
InnerException Exception The next error in the StackTrace. You can use InnerException to drill down through the complete list of exceptions to the originalException.
GetBaseException() Exception The Exception describing the original error. Since the original error might be wrapped up in the depths of InnerException, this method is essential to cut to the chase.
ToString() String Concatenates the above properties into a single string.

To see how the Exception looks as it bubbles up through each layer, consider the following sample Page_Load (with debugging turned on):

Sample Microsoft® Visual Basic® code

Sub Page_Load(ByVal src As Object, ByVal e As EventArgs)
   Throw New ApplicationException("This is an unhandled exception.")
End Sub

Try…Catch…Fail (Exception.Source=” yk1wsaam”)

Message: "This is an unhandled exception."

Stack Trace

at ASP.Default_aspx.Page_Load(Object src, EventArgs e) in C:\dev\CustomErrors\Default.aspx:line 5

Page_Error (Exception.Source=” yk1wsaam”)

Message: "This is an unhandled exception."

Stack Trace

at ASP.Default2_aspx.Page_Load(Object src, EventArgs e) in C:\dev\CustomErrors\Default.aspx:line 5 
at System.Web.UI.Control.OnLoad(EventArgs e) 
at System.Web.UI.Control.LoadRecursive() 
at System.Web.UI.Page.ProcessRequestMain()

Application_Error (Exception.Source=”System.Web”)

Message: "Exception of type System.Web.HttpUnhandledException was thrown."

Stack Trace

at System.Web.UI.Page.HandleError(Exception e) 
at System.Web.UI.Page.ProcessRequestMain() 
at System.Web.UI.Page.ProcessRequest() 
at System.Web.UI.Page.ProcessRequest(HttpContext context) 
at System.Web.CallHandlerExecutionStep.System.Web.HttpApplication+IExecutionStep.Execute() 
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

At the Try…Catch…Fail layer, only the immediate Exception exists and Exception.Source is a random string of characters that ASP.NET uses to identify the IL version of Default.aspx.

At the Page level, the stack has unwound to show that the error happened during Page.ProcessRequestMain.

At the Application layer, something interesting happens. The Source changes to the slightly meaningful, “System.Web.” Everything above Page.ProcessRequestMain from the Page level exception has been rolled into the Page.HandleError(exception e) line. At the Application layer, that detail is still available by either using InnerException or getting straight to the original exception with GetBaseException().

Try…Catch…Finally

The sooner you catch and resolve an error, the better. There is a reason they are called exceptions and not rules. In most cases, you can stop problems before they become exceptions with simple validation. FileIO.File.Exists and String.Empty are your friends. When writing a new class, always provide a way to validate data. For example, this article’s sample code includes an IP address class (IPAddress.vb). While methods IsInRangeToLong, and ToIPAddress can throw exceptions, the Validate and IsEmptyOrZero functions do not. No one should rely on Try…Catch…Fail for validation.

When writing a line or block of code that could fail based on an uncontrollable condition, such as an unavailable object (like a missing database connection or Web service), it should be wrapped with Try…Catch…Finally.

Inside the Catch block, you have the option of throwing the exception higher to a Page and perhaps an Application error handler. While you can still recover gracefully (that is, without an apology) inside a Page_Error handler, you are not likely to write one for every page. No, the main reason to throw the Exception higher than the Catch block is to perform notification or logging in an Application_Error handler.

When throwing an Exception higher, don’t:

Throw New ApplicationException(e)

Do:

Throw

The former is the syntax for a brand new custom Exception. In a Catch you already have one in front of you. The latter is all you need to send the current Exception merrily up the chain of command for further handling. If you must add your own two cents, you can always:

   Catch e As System.Exception
      Throw New System.ApplicationException("My Two Cents", e)

This creates a new Exception that wraps up the original error as its InnerException. Now for a proper demonstration of Try…Catch…Finally.

Visual Basic:

Dim Conn as New _
SqlConnection("Server=sql.mySite.com;uid=myUser;password=myPass")
Dim Cmd As New _
SqlCommand("SELECT Title, URL FROM Article ORDER BY Title", Conn)
Dim IsDbAvailable as Boolean = True
try
   ' The database may not be available
Conn.Open
ArticleGrid.DataSource = Cmd.ExecuteReader
ArticleGrid.DataBind
catch e as Exception
   ' Executed if an error occurs
   IsDbAvailable = False
   Trace.Write ("Database unavailable with Message: ", e.Message)
Trace.Write ("Stack Trace: ", e.StackTrace)

   ' Throw the exception higher for logging and notification
   Throw
finally
   ' If any clean-up is required for either case (unmanaged objects 
   ' left open, etc.), do it here.
Conn.Close()
end try

C#:

New SqlConnection("Server=sql.mySite.com;uid=myUser;password=myPass") Conn;
New SqlCommand("SELECT Title, URL FROM Article ORDER BY Title", Conn) Cmd;

Boolean IsDbAvailable = True;
try {
   // The database may not be available
Conn.Open();
ArticleGrid.DataSource = Cmd.ExecuteReader();
ArticleGrid.DataBind();
}
catch (e as Exception) {
   // Executed if an error occurs
   IsDbAvailable = False;
   Trace.Write ("Database unavailable with Message: ", e.Message);
   Trace.Write ("Stack Trace: ", e.StackTrace);

   // Throw the exception higher for logging and notification
   throw;
}
finally {
   /* If any clean-up is required for either case (unmanaged objects 
      left open, etc.), do it here. */
   Conn.Close ()
}

You can have multiple Catch blocks, each overloaded to catch a different Exception type (ExceptionIndexOutOfRangeExceptionNullReferenceException, and so on). Chris Sully wrote an article, Error Handling in ASP.NET…, which both provides a reference table of exception types and makes terrific further reading.

Before going on, note that there is a school of thought that says you should always throw an exception higher, that “swallowing” the exception is a bad idea. The reasoning is that exceptions should be visible and logged so that they can be guarded against in the future (with improved infrastructure, scalability, and so on), and that any instances that might be swallowed could be avoided with better programming. This happens to be true. Swallowing an exception is fine while debugging, but production code should always throw exceptions higher. Then, whenever preventable exceptions show up in the logs, you can apply some validation to ensure they won’t happen again.

Page_Error

Page_Error and Application_Error are similar. They take the same arguments, and they can even contain the same code. In fact a Page_Error section on a test page is a great way to debug code intended for Application_Error.

Their differences are few. One goes inside the .aspx file (or its code-behind), and the other inside global.asax (or an IHttpHandler class assembly). Differences between their stack traces were noted earlier, but if you always use GetBaseException(), then you will always have the original error, making this difference irrelevant. Another point to note is that in Page_Error you can prevent an exception from bubbling up further on to Application_Error by invoking Context.ClearError(). While useful for debugging (to avoid clogging the error log), this is not good practice in production code.

The following sample code adds the error to the trace information. The page will not finish loading once the exception is raised, so a brief apology is presented. Note that this simply shows the mechanics of Page_Error; actually helping the user out is an exercise left to the reader.

Visual Basic:

Sub Page_Load(ByVal src As Object, ByVal args As EventArgs)
   Throw New ApplicationException("This is an unhandled exception.")
End Sub

Sub Page_Error(ByVal src As Object, ByVal args As EventArgs) Handles MyBase.Error
   Dim e As System.Exception = Server.GetLastError()
   Trace.Write("Message", e.Message)
   Trace.Write("Source", e.Source)
   Trace.Write("Stack Trace", e.StackTrace)
   Response.Write("Sorry, an error was encountered.")
Context.ClearError()
End Sub

C#:

void Page_Load(Object src, EventArgs args) {
   // raise an intentional exception to get the ball rolling
   throw new ApplicationException("This is an unhandled exception.");
}

void Page_Error(Object sender, EventArgs args) {
   Response.Write("Error:\n");
   Exception e = Server.GetLastError();
   Trace.Write("Message",e.Message);
   Trace.Write("Source",e.Source);
   Trace.Write("Stack Trace",e.StackTrace);
Response.Write("Sorry, an error was encountered.");
   Context.ClearError();
}

Some people believe that putting code into an override of the Page‘s OnError event is equivalent to putting it in the Page_Error event. It isn’t. OnError hands control to a privateHandleError method. This is where ASP.NET checks whether customErrors is turned on and redirects when an exception is raised. It is where ASP.NET checks whether tracing is turned on and adds its own bit about the exception just raised. The main reason to override OnError is to replace this behavior with your own (described later). There is no good reason to put other code there. If someone does decide to turn on the built-in customErrors, code sitting in OnError will get in the way. If someone decides to add code toPage_Error it will not fire, as OnError fires first. To do error-handling at the Page level, use Page_Error.

global.asax: Application_Error

Generating e-mail notification, logging errors to the Event Log, and the structure of global.asax are covered wonderfully in many books and articles. Here are two good references:

The source code provided with the article implements these in global.asax and uses these web.config declarations:

<appSettings>
  <add key="customErrorAutomaticLogging" value = "On/Off" />
  <add key="customErrorAutomaticEmail" value="On/Off"/>
  <add key="customErrorEmailAddress" value="errors@mySite.com" />
</appSettings>

The interesting part is the construction of rich error messages.

Rich Custom Error Pages

There are four pieces to the puzzle:

  1. Configuring behavior in web.config. The choices are either to use the built-in customErrors tag with its mode and defaultRedirect properties, or to build custom settings in the appSettings section.
  2. Capturing, logging, and storing the Exception (in global.asax). The Exception needs to be stored inside an object that will persist until the custom error page. Possibilities are: ApplicationContextCookies, and QueryString. Other notification (e-mail, pager, and so on) can occur here too.
  3. Passing control from global.asax to the custom error page. Methods include using the built-in customErrors method, Server.Transfer(), or Response.Redirect().
  4. Retrieving and displaying the custom error message. Logic to display detailed information only to certain IP addresses may be included here.

Aa479319.customerrors_01(en-us,MSDN.10).gif

 

 

Figure 1. Flow of custom error handler

Unfortunately, you cannot pick and choose among all these options; some can only be implemented certain ways. For example, you cannot store the Exception in Context.Itemsand retrieve it after a Response.Redirect() since the Redirect() creates a brand new Context. The Exception would disappear. Here is a list of which storage baskets work with which control-passing methods:

Storage Basket Control-passing methods that work
Application Response.Redirect()Server.Transfer(), or customErrors:defaultRedirect
Cookies Response.Redirect()Server.Transfer(), or customErrors:defaultRedirect
ContextSession Server.Transfer()
QueryString Response.Redirect() or Server.Transfer()

While Application works with all three, it does not scale without a way to identify which session triggered the error. If two users trigger nearly simultaneous errors, you do not want one to overwrite the other. An advantage of Application is that it can store the complete Exception object.

Cookie storage also works with all three, and the client that triggered the error is guaranteed to see the proper error message. The caveats are that cookies must be enabled on the client, and they place an extra burden on bandwidth, which raises a scalability issue. Since you can only store strings and not complete objects to a cookie, you need to decide which strings to pass. XML serialization of an Exception is prevented by internal security issues, but binary or custom serialization is an option.

Context and Session, while limited to Server.Transfer(), both provide the advantage that the entire Exception object may be stored, without the client identification steps required to store the object to Application.

QueryString is an interesting option first described by Donny Mack in ASP.NET: Tips, Tutorials and Code (Mitchell, Mack, Walther, et al., SAMS Publishing, 2001). While you can’t stuff a complete Exception into QueryString and must choose which strings to pass, it does avoid local storage overhead completely and would seem to scale the best.

The sample code provided with this article implements all of these methods except Session, which would not be an improvement on Context.

Implementation

Now let’s look in more detail at the four steps mentioned above to construct rich custom error pages.

Configuring Behavior

The customErrors setting has three options for the mode property: On, Off, and RemoteOnly. The defaultRedirect property sets the custom error page. You can usecustomErrors in combination with your own appSettings, which you might do to control events in global.asax (like event logging). ASP.NET transfers control from global.asax to the custom page by generating a default OnError method in the Page‘s event cycle. You can replace the generated method with your own. The code goes something like this (if “myErrorPage.aspx” is the defaultRedirect value):

Visual Basic:

Protected Overrides Sub OnError(ByVal args As EventArgs)
    Response.Redirect("myErrorPage.aspx?aspxerrorpath=" & _
        Request.Path, True)
End Sub

C#:

protected override void OnError(EventArgs args) {
    Response.Redirect("myErrorPage.aspx?aspxerrorpath=" + 
        Request.Path, true);
}

The True tells ASP.NET to end the current request and proceed. Omitting it would imply the default false, and require an extra line to explicitly call Response.End.

Unfortunately, there is no easy way to override the Page OnError event for all pages on a site (the ideal way to replace customErrors), short of subclassing Page, which would require a new reference at the top of each page. The source code provided uses a different method, relying instead on custom appSettings inside web.config:

   <appSettings>
      <add key="customErrorAutomaticLogging" value="On/Off" />
      <add key="customErrorAutomaticEmail" value="On/Off" />

      <add key="customErrorMethod" 
value="Application/Context/Cookie/QueryString/Off" />
      <add key="customErrorPage" value="myErrorPage.aspx" />
<add key="customErrorBranchMethod" value="Redirect/Transfer" />
      <add key="customErrorAllowReport" value="On/Off" />
      <add key="customErrorEmailAddress" value="errors@mySite.com" />
</appSettings>

Using appSettings in code is as simple as this:

if (System.Configuration.ConfigurationSettings.AppSettings _
("customErrorAutomaticLogging").ToLower) = "on"

Settings can be added for other functions. For example, you might need to define a connection string for a central error log on a particular server.

Capturing, Logging, and Storing

The top of global.asax imports three namespaces: System.IOSystem.Diagnostics, and Msdn.ErrorIO. The Msdn.ErrorIO class is provided in the download as ErrorIO.vb.

This line of Application_Error captures the Exception and converts it to a string for logging or notification:

   Dim objError As Exception = Server.GetLastError.GetBaseException

In C# this is written over two lines:

   Exception objError = Server.GetLastError();
objError = objError.GetBaseException();

It was shown that the Page and Application layers stuff the original Exception in their own Exception wrappers as the call stack unwinds. While Server.GetLastError returns this whole wrapped package, GetBaseException pulls out the original Exception that interrupted execution.

The error is next logged and e-mailed to an administrator depending on web.config settings (see the section, global.asax: Application_Error). Functions WriteErrorToLog() andEmailError() are included in global.asax. Note that their catch blocks are empty, effectively swallowing any problems with writing to the event log or sending e-mail. This is done to avoid throwing a new Exception inside the handler and either creating an endless loop or having ASP.NET default to its own handler. However, it is never preferred to swallow errors. One solution would be to store a message (for example, HandlerMessage) along with the Exception to describe any problems with the exception handler itself. Such a message could appear with the original error on the custom error page. The WriteErrorToLog and EmailError functions allow for this by returning a true or false based on their success, though this feature is not implemented in the source provided.

Four storage models are implemented in the source code. You will find each represented by a class in ErrorIO.vbErrorApplicationErrorCookieErrorContext, andErrorQueryString. Since all classes use the same methods (StoreRetrieve, and Clear), a single interface (IErrorIOHandler) is provided for all three, plus an ErrorIOFactory class with a Create(model) method that allows the storage model to be selected at runtime. The advantage of using the Factory Pattern here is that you can declare the model to use in web.config and switch between them freely. Switching to a new model does not require changing any source code, only the web.config declaration. Of course you can still explicitly choose a model by creating an instance of, say, ErrorContext and using its StoreRetrieve, and Clear methods directly.

In the source, the storage basket is created like this:

   Dim objErrorIOFactory As New Msdn.RichErrors.ErrorIOFactory
   Dim objErrorBasket As MSDN.RichErrors.IErrorIOHandler
   objErrorBasket = objErrorIOFactory.Create(strErrorMethod)

Yes, it takes three lines to create an ErrorIOFactory, create a storage basket, and to connect the object created by the Factory to the basket. It’s the price of flexibility. As an alternative, you can hardcode the type of basket used, like so:

   Dim objErrorBasket as ErrorContext = New ErrorContext()

Instead of ErrorContext, you could similarly create an instance of ErrorApplicationErrorCookie, or ErrorQueryString. Whichever you choose, this is how the exception is stored:

   Dim strRedirect, strQueryString, strFilePath As String 
   strRedirect = AppSettings("customErrorPage")
   strQueryString = objErrorBasket.Store(objError)
   strFilePath = strRedirect & strQueryString

Hmmm, that probably wasn’t what you expected. You may be asking, “What’s this about building strFilePath? The result of objErrorBasket.Store() is a QueryString?!” Well, back in the section on configuring behavior, you saw the default ASP.NET way of implementing customErrors, and it went like this:

Response.Redirect("myErrorPage.aspx?aspxerrorpath=" & Request.Path, True)

To recreate this behavior, each Store() method in the ErrorIO classes generates the aspxerrorpath parameter. The ErrorQueryString class goes a step further and stores parts of the error (MessageSource, and StackTrace) plus a DateTime stamp in additional parameters. Combining the customErrorPage defined in web.config with the query string generated during the Store() results in a string that can be used with either Response.Redirect(strFilePath) or Server.Transfer(strFilePath).

Refer to the source to see exactly how each of the four storage baskets work; each has unique traits. When naming Application identifiers, ErrorApplication.Store() appends the client IP address so each client is sure to receive his own message. ErrorCookie.Store() uses one multi-part cookie rather than four separate cookies to get the job done.ErrorContext.Store() is the simplest the bunch; nothing tricky about it. QueryString.Store() uses a fast StringBuilder to generate the query string, which it initializes as 512 chars (the default is 16) to cut down on internal resizing steps.

Since some of the storage baskets provided store strings and not objects, the decision was made to store: Exception.MessageException.SourceException.StackTrace, theDate.Now marking the Exception, and the Request.Filepath (the Web page requested). The source can be modified to record other properties of Exception, the server name, client IP, or whatever meets your debugging requirements.

The web.config <appSettings> tag to turn rich custom errors on and set the storage basket is:

<add key="customErrorMethod" 
value="Application/Cookie/Context/QueryString/Off" />

Setting this value to Off prevents the remaining steps from occurring, but will not get in the way of any automatic logging or notification already done.

Passing Control

The final task of Application_Error is to execute the Redirect() or Transfer(). From the discussion of configuring behavior above, you already know that which of these you choose is tied to how the Exception is stored, and that some combinations work while others don’t. The features of the storage methods usually drive the decision.

But occasionally, features of Redirect() and Transfer() drive the decision. Redirect creates a new ContextTransfer does not. Redirect requires a round-trip to the browser,Transfer does not. As a result of this round-trip, Redirect rewrites the URL to reflect the location of the error page, Transfer does not.

If this seems to be an argument in favor of Transfer, it isn’t. The built-in customErrors feature uses Redirect and not Transfer for a reason. The rationale of the ASP.NET development team is that Redirect accurately displays the URL of the custom error page, while Server.Transfer is intended for “switchboard”-style pages (as on content management sites) where the true URL is preferably hidden.

Therefore, while Context is one of the more convenient ways to move the Exception from Application_Error to the rich error page, its dependence on Server.Transfer() makes it less than perfect. While you may not be concerned that this approach breaks a tenet of the design philosophy of ASP.NET, be aware that it does.

The web.config <appSettings> tags to set the control-passing method and identify the custom error pages are:

<add key="customErrorBranchMethod" value="Redirect/Transfer" />
<add key="customErrorPage" value ="myErrorPage.aspx" />

Retrieving and Displaying

Finally, the Exception must be retrieved from its storage basket and displayed in the rich error page. In the sample, this page is called myErrorPage.aspx.

In the sample code, the Exception is retrieved in myErrorPage.aspx, inside the DisplayDetailedError() function.

   Dim objErrorIOFactory As New Msdn.ErrorIO.ErrorIOFactory
   Dim objErrorBasket As Msdn.ErrorIO.IErrorIOHandler
objErrorBasket = objErrorIOFactory.Create( _ 
System.Configuration.ConfigurationSettings.AppSettings("customErrorMethod") )
   objErrorBasket.Retrieve(strMessage, strSource, 
     strStackTrace, strDate, strQueryString)
   objErrorBasket.Clear()

The first three lines are familiar; a similar series was used to create the basket to Store() the Exception.

Retrieve() passes five parameters by reference. These will come back holding the exception data. The actual Retrieve methods (in ErrorIO.vb) are similar to their Store()counterparts. The big difference is the use of Try…Catch…Fail to swallow errors that might occur while retrieving data from the baskets. Again, it is not desirable for an exception handler to throw exceptions of its own. If the Retrieve() fails, the error data returned instead describes the type of retrieve attempted (ApplicationCookieContext, orQueryString). Try…Catch…Fail is not used in QueryString.Retrieve(), it being the only technique that cannot generate exceptions of its own.

Finally objErrorBasket.Clear is called. Two of the storage baskets can or should be destroyed explicitly. Since Application is unique for each client (it uses the IP Address to name each identifier), it must be destroyed once used, and even then there is a chance this technique will leak memory.

So too should cookies be destroyed. Cookies are set to expire in thirty minutes, but a user can trigger any number of errors in this span. Thirty seconds would make more sense, but the expiration is based on the server’s clock, not the client’s. Have you seen how far off some system clocks are? Thirty minutes is a realistic term, most clocks should be within that span of each other. But this is the problem with short-term cookies. Explicit destruction is the best answer.

ErrorContext.Clear() and ErrorQueryString.Clear() have nothing to do, as these baskets ceases to exist once the rich error page is emitted to the client.

The sample rich error page has three display features:

  • Standard Error Message. The standard message, intended for public consumption, explains that an error occurred while loading the requested page and provides a link to try loading the page again. Links are also provided to get the user back to familiar ground—in this case back to the home page. If your site has standard navigation or menu bars, be sure to include them in the error page as well.The last thing you want is for someone to hit a brick wall of no return. Get the user back on track. Some sites parse the page request to seed a search of either the current site or the whole Web with a message like, “We couldn’t service your request but maybe this will help.” Helpful is good.
  • Error Report Panel. A form asking the user what was expected when the error happened is a good way to trace issues. This sample panel says, “If you describe what you were trying to do, perhaps it can be fixed.” Reports are e-mailed to the administrator along with the Exception.ToString data. The code in SendClick(), which sends the actual message, is similar to the EmailError() function in global.asax. This feature is configured with the web.config <appSettings> keys:

    <add key="customErrorAllowReport" value="On"/>
    <add key="customErrorEmailAddress" value="errors@mySite.com"/>
  • Detailed Error Panel. This panel (populated in the DisplayDetailedError() function) reports the exception raised by the error, as retrieved from the storage basket. This information is useful to those debugging the site, but not the general public. While you could add authentication to distinguish the two, not all sites need authentication. The built-in customErrors feature has a switch (mode="remoteOnly") to only display detailed information to the client at 10.0.0.1, but developers rarely have access to the server console (Why are so many errors only reproducible on the live server?). The way to overcome this limitation is to make a custom IP address class with range checking. Table 2 shows the methods for this class.Table 2. IPAddress Class (namespace: Msdn.IPUtils, source: IPAddress.vb)
    Method Description
    Validate() Test for a valid IP address with a regular expression.
    IsInRange() Check an IP address against a specified range.
    ToString() Return a value of type String.
    ToLong() Returns the IP address as a 12-digit Long (for example, “10.0.0.255” becomes 010000000255). Helpful for a variety of comparisons.
    ToIPAddress() Returns a value of type System.Network.IPAddress.

    This panel is displayed if the client’s IP address is within the range declared by web.config <appSettings>:

    <add key=”customErrorIpRangeMin” value=”10.0.0.1″/>

    <add key=”customErrorIpRangeMax” value=”10.0.0.255″/>

The Return of Page_Error

Debugging custom error pages is a pain. In addition to a local Page_Error handler, it helps to turn on tracing and debugging so you can capture and display problems without throwing the Exception all the way up to Application_Error. Once your custom error page works, turn these off again. While you can just forget about Page_Error, you will be glad it’s there during routine maintenance, or whenever you add new errors.

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


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/elmahfor 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 theError 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 /bindirectory 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 themachine.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 theErrorLogPageFactory 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 ErrorLoginstance 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 ErrorLogand 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 SqlErrorLogMemoryErrorLog, 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 ErrorLogEntryinstances.

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 NameValueCollection of Web server variables, such as those contained in HttpRequest.ServerVariables.
QueryString NameValueCollection of HTTP query string variables, such as those contained in HttpRequest.QueryString.
Form NameValueCollection of form variables, such as those contained in HttpRequest.Form.
Cookies 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 ErrorMailModuleErrorLogModule is an HTTP module that creates an event handler for the application’s Errorevent. 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 theHttpContext instance specific to the particular request. LogException() simply calls the appropriate ErrorLog class’s Log() method, passing in a new Error instance. (TheError 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.DefaultDefault is a static property of type ErrorLogin the ErrorLog class. It consults the Web application’s configuration to determine what class to use for exception logging: SqlErrorLogMemoryErrorLog, 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

, 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—ErrorDetailPageErrorHtmlPageErrorRssHandlerErrorLogPage, 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:

  • <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.
  • <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 connectionStringproperty 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!