1.6. Building a Custom ASP.NET Exception Handler

In many programming situations, when a built-in framework feature doesn't suit your needs, you have the option of rolling your own. Once you've decided to do that, you have the freedom to implement any functionality you need. Let's take a moment to jot down what you think an ideal custom exception handler should be and do — a "wish list," if you will.

  • It should capture other information about the state of the application at the point of failure, besides just the exception.

  • It should allow you to redirect to a nice custom error page of your choosing, using either Response.Redirect or Server.Transfer as you see fit.

  • It should store the exception and all of the other captured information in state, so you can retrieve it from the custom error page.

  • It should provide a number of state-handling mechanisms and let you choose the one you want.

  • It should provide various ways to distinguish "normal" users from developers or administrators, either using the ASP.NET authentication system or an alternate method if authentication is not being used for the site.

  • It should use the same custom error page to provide detailed error information to developers, while shielding those details from normal users.

  • It should give normal users an opportunity to send a help request to administrators that includes all of the error details behind the scenes.

  • It should give you the option of passing HTTP errors that you specify to their own custom pages, like the ASP.NET default exception handler does.

  • It should allow you to specify that a notification be sent to the site administrators when an unhandled exception occurs.

  • It should provide various notification methods such as e-mail or SMS text message. It should customize the message so that e-mails would include all of the captured error details, while a text message would only include a short summary.

  • It should allow you to log the error information to your choice of a database or a text-based file. In cases in which you are running a site on your own server, it should allow logging to the Windows Event Log as well.

  • There should be data access methods that let you retrieve log records easily, so that you can build a rich custom administration dashboard that lets you list, page, and sort through the error logs, as well as display all the details of any individual error.

  • Optionally, you could create consolidated data access methods that let you retrieve error records across multiple logs at the same time.

  • It should not require hard-coding of any of the above options; instead, all functionality should be configurable from a custom section in web.config. However, default values should still be provided for common options, so that not too much configuration is needed to get it up and running quickly.

  • It should be developed as a class library, so you can reuse it across any web site you develop.

Now that is a tall list of orders! Fortunately, dear reader, you don't have to write this program. In the pages that follow, you'll look at some of the internal workings of the application featured in the code download for this Wrox Blox, which provides all of the features listed above.

The download is actually a Visual Studio solution that contains two projects: a class library project called LD.ExceptionHandling that contains all of the custom exception handling code, and a sample web site for a fictitious company called Fourth Coffee that uses the LD.ExceptionHandling.dll assembly and demonstrates its functionality. This solution should open and run in Visual Studio 2008 or in Visual Web Developer 2008.

The sample web site assumes that you have an SQL Server instance named SQLEXPRESS and that user instances are enabled. If that's not the case for your setup, be sure to edit the LocalSqlServer connection string in web.config accordingly.

1.6.1. The "Fourth Coffee" Sample Site

Once you open the solution, go ahead and run the SampleApp web site (or browse Default.aspx). There, you should see a page like that shown in Figure 5.

Figure 5. The Fourth Coffee sample site.

You'll find several demos on the home page that demonstrate how the application behaves under various error conditions. Take a few moments, try the demos, and play around a little bit. Of course, when you first visit the site, it will treat you as an unauthenticated visitor, so when you click on Demo 1, you should see something like the screen shown in Figure 6.

Figure 6. The Fourth Coffee error page.

You'll see that it provides a friendly and very non-intimidating message. It tells you where the error occurred and provides a link to that page. It reassures you that the error has been logged for later review. It presents various options for where to go from here. It lets you provide feedback and even request a reply if you wish. And because the error page fits right into the site, all of the navigation and sidebar features are available as well.

Once you've seen all the demos, go ahead and click on the "Login" link in the upper-right corner. There are credentials shown on the login page that grant you administrative privileges. Then, click on the "Home" link in the menu to return to Default.aspx. If you run Demo 1 again, you will see something much different, as shown in Figure 7.

Figure 7. The Fourth Coffee error page (logged in as Administrator).

This is the same error page. The upper part is similar, but the feedback/options area has been replaced by a summary of the exception. Note that there's a link at the bottom that lets you obtain more detailed information. Click on that, and you see the display shown in Figure 8.

Figure 8. Error details.

This page shows a detailed error report, including information about the exception you just raised, along with data about the request, the user, and the client at the time the error occurred. There is also a link button that lets you delete this error from the log.

Near the bottom of the page, there is a hyperlink to go to the error log list, as shown in Figure 9.

Figure 9. Error log.

Here, you'll find a sortable, pageable grid, with each row representing a different unhandled exception that has occurred in the application. It shows the date and time of the exception, the file it was thrown from, and the exception's type and message. You'll notice there is a "Details" link at the end of each row (you may have to scroll the grid horizontally to see them) that takes you to an individual report like that shown in Figure 8. Under the grid, there are controls to manage the list by deleting all error records that occurred before a specified date, or to clear the log completely.

The "Admin" menu link should also now be visible. Clicking on it reveals links to the error logs (see Figure 10).

Figure 10. Administration screen.

You will see three links here, which navigate to nearly identical pages. The main difference between these pages is the log store from which they retrieve their data.

Now that you've seen the custom exception handler in action, it's time to explore how it works in further detail.

1.6.2. The LD.ExceptionHandling Class Library

The other project in the solution is a class library called LD.ExceptionHandling. The sample web site contains a reference to this project, so that whenever you build the solution, the LD.ExceptionHandling.dll assembly in the site's Bin file is updated automatically.

Despite the number of classes, its basic operation is quite simple:

  • Unhandled exceptions are trapped in the HttpApplication.Error event delegate of the ErrorModule class.

  • An object of type ErrorData is instantiated to hold the exception information. The ErrorData constructor collects a variety of information about the request, and holds that as well.

  • An object deriving from type ExceptionHandler is created to temporarily store the ErrorData object, so that it will be available to the custom error page.

  • A re-direct to the custom error page is performed.

  • The error information is logged, and notifications are sent.

Once a request arrives at the custom error page, you would typically create another ExceptionHandler to retrieve the ErrorData object. You can then use the properties from Settings.ExceptionSettings to determine how the error data should be presented.

As you can probably tell from the above explanation, the ErrorModule class, ErrorData class, and the derived ExceptionHandler classes comprise the majority of the handling work. The other classes supplement this main functionality by providing notification, logging, and configuration support.

1.6.2.1. Custom Configuration

Before getting into the "meat and potatoes," so to speak, it would benefit you to understand how the LD.ExceptionHandling assembly is configured, as the various options are used throughout — both internally and as public properties available to any web site that consumes it.

The program obtains the required values from a custom configuration section placed in the web.config file. A typical usage is shown in Listing 21.

Example 21. Configuring the LD.ExceptionHandling custom exception handler
<exceptionHandling
   pathToErrorPage="~/ErrorPages/ErrorPage.aspx"
   httpStatusCodesToIgnore="403,404" >
<logging loggingMethod="Database" />
<notification
   emailNotificationAddresses="[email protected],[email protected]"
   smsNotificationAddresses="[email protected]"
   userFeedbackEmailAddresses="[email protected]" />
<privileges
   privilegedRoles="Administrators" />
</exceptionHandling>

There are many other attributes available. In actual practice, you probably won't need to explicitly specify all of them, as many common default values are already defined for undeclared attributes. The following list describes each attribute in detail.

1.6.2.1.1. ExceptionHandling Section
  • pathToErrorPage — Specifies the path to the error page used by the custom exception handler. This can be either a relative URL ("ErrorPage.aspx") or a virtual path ("∼/ErrorPage.aspx"). Not specifying this value disables the re-direct functionality of the handler, which causes control of unhandled exceptions to pass to the ASP.NET default exception handler. However, the logging and error notification functions would still be available.

  • redirectMethod — Specifies the mechanism used to redirect the request to the custom error page.

    • Allowed Values — Redirect|Transfer

    • Default Value — Redirect

  • handlerType — Specifies the state-handling mechanism used to temporarily store error data.

    • Allowed Values — Cache | Application | Cookie | Context | Session | QueryString

    • Default Value — Cache

  • httpStatusCodesToIgnore — A comma-delimited list specifying the HTTP error status codes that will not be handled, and therefore passed to the ASP.NET default exception handler. You would typically specify 400-level codes here, such as "403, 404".

1.6.2.1.2. Logging Subsection
  • eventLoggingEnabled — Whether to log errors to the Windows Event Log. While this would not be useful (or even allowed) in a shared hosting scenario, it's an available option if you run your site on your own server.

    • Allowed Values — true|false

    • Default Value — false

  • loggingMethod — The error logging mechanism to use. Leaving this value undeclared or specifying "None" disables error logging.

    • Allowed Values — Database | Xml | None

    • Default Value — None

  • dbLogConnectionStringName — The connection string name (as defined in the <connectionStrings> section) used to connect to the database that contains the error log table. This lets you specify a different connection or database for error logging from the one used by the rest of your application.

    • Default Value — LocalSqlServer

  • dbLogTableName — The name of the error log table in the database pointed to by the above connection string. You need to set up the table in advance. (An SQL script is provided with the download that will create the required table for you.) You only need to specify this value if you set up your table with a different name from the default value.

    • Default Value — LD_ExceptionHandling_Errors

  • pathToXmlLog — The path to the text file that stores the XML log. Again, this can be a relative URL or a virtual path. You do not need to set this file up in advance; if the program attempts to access it and it doesn't exist, the directory and file specified by this attribute will be created and automatically granted the appropriate permissions. However, if the directory you intend to use already exists, you need to ensure that the ASP.NET account has read/write/modify privileges on it.

    • Default Value — ∼/App_Data/LD_ExceptionHandling_Errors.xml

1.6.2.1.3. Notification Subsection
  • emailNotificationAddresses — A comma-delimited list of e-mail addresses that should be notified when an unhandled exception is processed. Leaving this value undeclared (or specifying an empty string) disables e-mail notification.

  • smsNotificationAddresses — A comma-delimited list of SMS addresses that should be notified when an unhandled exception is processed. Leaving this value undeclared (or specifying an empty string) disables SMS notification.

  • userFeedbackEmailAddresses — A comma-delimited list of e-mail addresses that should receive user feedback sent from the error page. Leaving this value undeclared (or specifying an empty string) disables user feedback.

1.6.2.1.4. Privileges Subsection
  • privilegedRoles — A comma-delimited list of roles, where users belonging to these roles should be allowed to see detailed error information

  • privilegedUsers — A comma-delimited list of usernames, which specify individual users who may or may not belong to one of the roles indicated above, but who should still be allowed to see detailed error information

  • privilegedIPs — A comma-delimited list of IP addresses, requests from which should be allowed to see detailed error information. In most cases, using roles or usernames is a more convenient and reliable solution; however, using the IP address is useful for sites that don't use authentication or roles.

  • treatLocalUsersAsPrivileged — A Boolean value indicating whether local requests should be treated as privileged, regardless of username, role, or IP. Setting this to true provides the same functionality as mode="RemoteOnly" in the ASP.NET default exception handler.

    • Default Value — true

Keep in mind that while you set these options from web.config, most of the actual values are consumed internally by the LD.ExceptionHandling assembly. The static Settings class only exposes a limited number of public Boolean read-only properties that encapsulate some of the configured options, as follows:

  • ExceptionSettings.Logging.LoggingEnabledtrue if loggingMethod is set to "Database" or "Xml"; otherwise, false

  • ExceptionSettings.Logging.EventLoggingEnabledtrue if eventLoggingEnabled is set to true; otherwise, false

  • ExceptionSettings.Notification.EmailNotificationEnabledtrue if emailNotificationAddresses is not empty; otherwise, false

  • ExceptionSettings.Notification.SmsNotificationEnabledtrue if smsNotificationAddresses is not empty; otherwise, false

  • ExceptionSettings.Notification.UserFeedbackEnabledtrue if userFeedbackEmailAddresses is not empty; otherwise, false

  • ExceptionSettings.Privileges.CurrentUserIsPrivilegedtrue if any of the following conditions are met; otherwise, false:

    • The current request is local and treatLocalUserAsPrivileged is true.

    • The current user's IP address is contained in privilegedIPs.

    • The current user's username is contained in privilegedUsers.

    • The current user's role is contained in privilegedRoles.

What's an "SMS Address"?

Instead of using an SMS gateway (which generally costs money), this project uses the E-mail-to-Text capability provided by most of the major mobile carriers, which is totally free.

An SMS address is no different from a regular e-mail address. It consists of a 10-digit mobile number, the @ symbol, and a special domain name that corresponds to a particular carrier. For example, if your mobile number is 212-555-1234 and you're on AT&T, I could text you by sending an e-mail to [email protected].

Here are the domains for the most popular U.S. mobile carriers:


1.6.2.2. Handling and Re-Directing

The ErrorModule class is an HttpModule that defines an event handler for the HttpApplication.Error event called Application_Error. Think of it as the "Grand Central Station" of unhandled exceptions. This is truly where all the action starts. Listing 22 shows the event handler.

Example 22. HttpApplication.Error event handler in the ErrorModule class
private static void Application_Error(object sender, EventArgs e)
{
   HttpContext context = HttpContext.Current;

   Exception exception = context.Server.GetLastError().InnerException ??
                         context.Server.GetLastError();

   if (exception is HttpException)
   {
      HttpException httpException = (HttpException) exception;
      int statusCode = httpException.GetHttpCode();

      int[] statusCodesToIgnore = Array.ConvertAll(
         Settings.ExceptionSettings.HttpStatusCodesToIgnore.Split(','),
         delegate(string s) { return Int32.Parse(s); });

      bool ignoreStatusCode = Array.Exists(
         statusCodesToIgnore,
         delegate(int code) { return code == statusCode; });

      if (ignoreStatusCode)
      {
         return;
         // Note: this will pass control to customErrors.
         // Error won't be logged.
      }
   }

   ErrorData errorData = new ErrorData(exception);

   ExceptionHandler exceptionHandler = ExceptionHandler.Create();

   string queryString;
   exceptionHandler.SaveErrorData(errorData, out queryString);

   if (Settings.ExceptionSettings.PathToErrorPage != String.Empty)
   {
      string redirectPath = Settings.ExceptionSettings.PathToErrorPage +
                            queryString;

      if (Settings.ExceptionSettings.RedirectMethod ==
          RedirectMethod.Redirect)
      {
         context.Response.Redirect(redirectPath);
      }
      else if (Settings.ExceptionSettings.RedirectMethod ==
               RedirectMethod.Transfer)
      {
         context.Server.Transfer(redirectPath, false);
      }

}

   ErrorUtilities.LogAndNotify(errorData);

   // Note: Should an exception be thrown from any of the above code
   // before the redirect takes place, control would pass to customErrors.
}

The logic in Listing 22 shouldn't be too hard to follow. First, a reference is obtained to the unhandled exception. As you've already learned, this exception generally comes wrapped as an HttpUnhandledException. The reason this wrapping takes place is that the ASP.NET default exception handler is built to work with exceptions of that type.

But what I didn't mention before is that there are a few special cases in which exceptions that are of type HttpException do not get wrapped. HttpException is a special class that encapsulates errors encountered during the processing of HTTP requests — in other words, requests that are answered with response codes in the range 400–599, which represent errors reported by the client or Web server. Some of the 4xx-level exceptions, such as those caused by HTTP 403 (Forbidden) and HTTP 404 (Not Found), are passed to the HttpApplication.Error event unwrapped. These are the so-called "white screen" errors, which the ASP.NET default exception handler treats differently, as shown in Figure 11.

Figure 11. An ASP.NET default "white screen" error (HTTP 404).

Our custom exception handler gives you the option of treating these errors differently too, by specifying certain codes in the httpStatusCodesToIgnore configuration attribute. For example, you probably wouldn't want to choke up your logs or get e-mail for every "file not found" or "forbidden request" error that pops up. Also, displaying special pages tailored to these errors is usually a good idea. To accomplish that, you could specify "403, 404" for this attribute.

So, the next part of the Application_Error method checks if the current exception is in the list of status codes that you want to ignore, and if it is, bypasses the later re-direct. The return statement causes control to pass to the ASP.NET default handler.

By the way, if you specify some status codes to ignore, I strongly suggest that you handle them in the default handler, as I'll explain later; otherwise, you'll get the not-very-impressive white screen like the one in Figure 11.

Once it's been determined that the exception should be handled, a new ErrorData object is instantiated, which encapsulates information about the exception and the current request (as you'll see in more detail shortly). An ExceptionHandler object is also created. The ExceptionHandler object uses its SaveErrorData method to temporarily save information from the ErrorData object, using the state handler you specified in the configuration. This method also returns a query string to append to the redirect, which takes place next.

Finally, logging and notification is performed in accordance with your configured options.

1.6.2.3. Collecting Information

While an exception holds valuable information about what kind of error has occurred and the methods in play when it occurred, it doesn't always tell you what may have caused the error in the first place. For example, there is a well-known issue in which a request to an ASP.NET page from the Google cache may under certain conditions cause a CryptographicException. However, there is nothing in the exception that could tell you where the request came from. For another example, there may be a situation in which certain form values may have caused a post to crash, but the exception can't tell you what values were posted from the form.

For these reasons, it's well worth gathering pertinent information about the nature of the request when an exception is thrown. Listing 23 shows the constructor for the ErrorData class where that takes place.

Example 23. ErrorData class constructor
public ErrorData(Exception exception)
{
   if (exception == null)
   {
      throw new ArgumentNullException(
         "exception",
         "The Exception argument cannot be null.");
   }

   HttpRequest request = HttpContext.Current.Request;

   this.type = exception.GetType().ToString();
   this.message = exception.Message;

this.source = exception.Source;
   this.stackTrace = exception.ToString().Replace("--->", "--->
");

   this.errorID = Guid.NewGuid();
   this.date = DateTime.Now;

   this.errorPath = request.FilePath;
   this.referrer = request.UrlReferrer != null
                      ? request.UrlReferrer.ToString()
                      : "None";
   this.anonymousID = !String.IsNullOrEmpty(request.AnonymousID)
                         ? request.AnonymousID
                         : "None";
   this.user =
      !String.IsNullOrEmpty(HttpContext.Current.User.Identity.Name)
         ? HttpContext.Current.User.Identity.Name
         : "Not authenticated";
   this.userIP = !String.IsNullOrEmpty(request.UserHostAddress)
                    ? request.UserHostAddress
                    : "Not available";
   this.client = !String.IsNullOrEmpty(request.Browser.Browser)
                    ? request.Browser.Browser + " " +
                      request.Browser.Version
                    : "Not available";
   this.clientPlatform = !String.IsNullOrEmpty(request.Browser.Platform)
                            ? request.Browser.Platform
                            : "Not available";
   this.userAgent = !String.IsNullOrEmpty(request.UserAgent)
                       ? request.UserAgent
                       : "Not available";
   this.httpMethod = !String.IsNullOrEmpty(request.HttpMethod)
                        ? request.HttpMethod
                        : "Not available";
   this.form = request.Form.Count > 0
                        ? HttpContext.Current.Server.HtmlEncode(
                             request.Form.ToString())
                        : "None";
   this.isSecureConnection = request.IsSecureConnection.ToString();
}

As you can see, the properties of the ErrorData object hold data from the passed-in exception, as well as information collected from other sources during its instantiation.

If it's not immediately evident why the individual properties of the exception are "broken out" rather than stored as a unitary property of ErrorData, it will become clear to you as you later examine the ExceptionHandler classes.

This is by no means an exhaustive list of the information that is available, but the most relevant properties are represented here. These should suffice for just about any situation, while keeping the error records reasonable in size. If you have a special need, you can always alter the application to add new properties to the ErrorData class.

One thing to note is the ErrorData.StackTrace property, which is populated from the ToString method of the exception rather than from the exception's own StackTrace property. This is because, unlike Exception.StackTrace, Exception.ToString provides a formatted trace of not only the current exception, but of the stack traces of every InnerException wrapped within it. This is invaluable for debugging purposes.

If you examine the ErrorData class code from the project, you'll note that it has its own ToString override. This provides a formatted string for notification e-mails and the Event Log, as well as being available for use in any page-level exception handlers you implement.

1.6.2.4. Storing and Retrieving

Once ErrorData is instantiated and its constructor collects the relevant data, you need to store the object so that it can be retrieved after the redirect from within the custom error page.

This is performed by the various classes located in the LD.ExceptionHandling.Handlers namespace. ExceptionHandler is an abstract class that provides stubs for methods called SaveErrorData, GetErrorData, and ClearErrorData that are implemented in several subclasses. It provides a few helper properties to those classes as well.

Being an abstract class, an instance of ExceptionHandler cannot be obtained directly with the new keyword. Instead, it uses a static factory method called Create to return an instance of a specific derived class, the type of which corresponds to the handlerType attribute you specify in the configuration. Listing 24 shows the Create method.

Example 24. ExceptionHandler.Create method
public static ExceptionHandler Create()
{
   string handlerType = Enum.GetName(typeof(HandlerType),
                                     Settings.ExceptionSettings.
                                        HandlerType);

   if (Settings.ExceptionSettings.RedirectMethod ==
       RedirectMethod.Redirect &&
       (Settings.ExceptionSettings.HandlerType == HandlerType.Session ||
        Settings.ExceptionSettings.HandlerType == HandlerType.Context))
   {
      throw new InvalidOperationException(
         "If you specify handlerType="" + handlerType +
         "", you must also specify redirectMethod="Transfer".");
   }

   return
      (ExceptionHandler)
      Activator.CreateInstance(
         Type.GetType("LD.ExceptionHandling.Handlers." + handlerType +
                      "Handler"));
}

If you inspect Listing 24, you see that the last statement returns an instance of a specific derived handler that is then cast to type ExceptionHandler. For example, if you specify handlerType="Cache" (the default), you get an instance of LD.ExceptionHandling.Handlers.CacheHandler. If you specify handlerType="Cookie", you get an LD.ExceptionHandling.Handlers.CookieHandler, and so on.

Note that the Create method throws an InvalidOperationException if you attempt to specify handlerType="Context" or handlerType="Session" with redirectMethod="Redirect". These combinations don't work, because Response.Redirect destroys the current HttpContext, so that any objects stored in HttpContext or HttpSessionState couldn't be subsequently retrieved.

Let's take a look at how the actual handling functionality is implemented by the derived classes. Listing 25 shows the CacheHandler class.

Example 25. The CacheHandler class
internal class CacheHandler : ExceptionHandler
{
   public override ErrorData GetErrorData()
   {
      ErrorData errorData =
         (ErrorData) Context.Cache["ErrorData" + UserIdentifier];
      if (errorData == null)
      {
         errorData = new ErrorData();
         errorData.Message = FailureMessage;
      }

      this.ClearErrorData();

      return errorData;
   }

   internal override void SaveErrorData(ErrorData errorData,
                                        out string queryString)
   {
      Context.Cache.Insert(
         "ErrorData" + UserIdentifier,
         errorData,
         null,
         DateTime.MaxValue,
         Cache.NoSlidingExpiration,
         CacheItemPriority.NotRemovable,
         null);

      queryString = "?aspxerrorpath=" + Path;
   }

   internal override void ClearErrorData()
   {
      Context.Cache.Remove("ErrorData" + UserIdentifier);
   }
}

This should be fairly self-explanatory. The SaveErrorData method override inserts an ErrorData object into the Cache that is tied to the current user. The GetErrorData method attempts to retrieve the ErrorData from the Cache. If it's not there for some reason, instead of throwing an exception, the method gets a new blank ErrorData object and sets a message indicating that the retrieval failed. As soon as the ErrorData object is retrieved, it is cleared from the Cache.

The ApplicationHandler, ContextHandler, and SessionHandler classes work in much the same way. However, the CookieHandler (as shown in Listing 26) and QueryStringHandler classes operate a bit differently.

Example 26. CookieHandler class
internal class CookieHandler : ExceptionHandler
{
   public override ErrorData GetErrorData()
   {
      HttpCookie errorCookie = Context.Request.Cookies["ErrorData"];
      ErrorData errorData = new ErrorData();
      if (errorCookie == null)
      {
         errorData.Message = FailureMessage;
      }
      else
      {
         errorData.Message = errorCookie.Values["Message"] ?? FailureMessage;
         errorData.Source = errorCookie.Values["Source"] ?? FailureMessage;
         errorData.StackTrace = errorCookie.Values["StackTrace"] ??
                                FailureMessage;

         errorData.Date = errorCookie.Values["Date"] == null
                             ? DateTime.MinValue
                             : DateTime.ParseExact(
                                  errorCookie.Values["Date"], "s",
                                  CultureInfo.InvariantCulture);

         errorData.ErrorID = errorCookie.Values["ErrorID"] == null
                                ? Guid.Empty
                                : new Guid(errorCookie.Values["ErrorID"]);
      }

      this.ClearErrorData();

      return errorData;
   }

   internal override void SaveErrorData(ErrorData errorData,
                                        out string queryString)
   {
      if (errorData == null)
      {
         throw new ArgumentNullException(

"errorData",
            "The ErrorData argument cannot be null.");
      }

      HttpCookie errorCookie = new HttpCookie("ErrorData");
      errorCookie.Values["Message"] = errorData.Message;
      errorCookie.Values["Source"] = errorData.Source;
      errorCookie.Values["StackTrace"] = errorData.StackTrace;
      errorCookie.Values["Date"] = errorData.Date.ToString("s");
      errorCookie.Values["ErrorID"] = errorData.ErrorID.ToString();
      Context.Response.Cookies.Add(errorCookie);

      queryString = "?aspxerrorpath=" + Path;
   }

   internal override void ClearErrorData()
   {
      HttpCookie errorCookie = new HttpCookie("ErrorData");
      errorCookie.Expires = DateTime.Now.AddDays(-1);
      Context.Response.Cookies.Add(errorCookie);
   }
}

A perusal of the CookieHandler class in Listing 26 should reveal why the ErrorData constructor splits up the exception, rather than holding it as a single property — a cookie can only store simple strings, not complex objects, so you couldn't store an "ErrorData.Exception" object as a cookie value. (Of course, the same limitation also applies to querystrings.)

1.6.2.5. Logging and Notification

The LD.ExceptionHandling framework also provides error logging and notification capabilities. By the way, if you haven't already noticed, the publicly accessible classes for handling, logging, and notification each reside in their own respective namespaces, and even the configuration is broken out into separate subsections. This should prompt you to think of handling, logging, and notification as three related yet separately implemented features, each of which you can enable, disable, and utilize independently of the others. For example, even though the handler is designed to work on unhandled exceptions, you could also use the logging or notification features for a handled exception, if you had a reason to do that. A demo of this is provided in the Fourth Coffee sample.

The real "heavy lifting" is all performed by the internal classes of the LD.ExceptionHandling.Persistence namespace. These contain the methods that work directly with the database, text files, the Windows Event Log, and your e-mail server. You can't actually reach into this persistence layer from your site. Instead, you can use the public static classes of the Logging and Notification namespaces, which provide business logic that lets you interact with these resources.

For example, you cannot call the Persistence.Logging.ErrorDataXml.InsertError method to log an error into the XML text log from an exception handler in your site code. You could, however, call the Logging.Logger.LogToXmlLog method to do so. LogToXmlLog calls the ErrorDataXml.InsertError method for you, as Listing 27 shows.

Example 27. LogToXmlLog method
public static void LogToXmlLog(ErrorData errorData)
{
   if (errorData == null)
   {
      throw new ArgumentNullException("errorData",
                                      "The ErrorData object cannot be null.");
   }

   ErrorDataXml.InsertError(errorData);
}

You might have noticed that the LogToXmlLog method in Listing 27 doesn't honor your <exceptionHandling> configuration. It doesn't check the value of ExceptionSettings.Logging.LoggingMethod to see what you've specified for the loggingMethod attribute in web.config, and it doesn't throw an exception if you haven't specified "Xml". That means you could conceivably use the LogToXmlLog method to "force" log an error to your XML file, even if you specified loggingMethod="Database" in the configuration, or even if you haven't specified loggingMethod at all. See Listing 28.

Example 28. Logging an error to the Xml log file
catch (ArrayTypeMismatchException ex)
{
   litMessage.Text = "Storing an integer in a string array is not allowed!";
   Logger.LogToXmlLog(new ErrorData(ex));
   // this works even if loggingMethod="Database" or "None" is set
}

Keep in mind that, while this is perfectly acceptable, most of the time you won't need (or want) to address the Logging or Notification classes directly. Instead, you'd probably want to use the methods of the ErrorUtilities class as shown in Listing 29, which automatically enforce your configuration options.

Example 29. Overloaded LogAndNotify method
public static void LogAndNotify(ErrorData errorData)
{
   LogAndNotify(errorData, true, true, true, true);
}

public static void LogAndNotify(
   ErrorData errorData,
   bool sendEmailNotification,
   bool sendSmsNotification,
   bool logToApplication,
   bool logToWindowsEventLog)
{
   if (Settings.ExceptionSettings.Notification.EmailNotificationEnabled &&
       sendEmailNotification)

{
      Notifier.SendEmail(
         errorData,
         "Error Notification from " + host,
         Settings.ExceptionSettings.Notification.
            EmailNotificationAddresses,
         null,
         null);
   }

   if (Settings.ExceptionSettings.Notification.SmsNotificationEnabled &&
       sendSmsNotification)
   {
      Notifier.SendSMS(errorData,
                       Settings.ExceptionSettings.Notification.
                          SmsNotificationAddresses);
   }

   if (logToApplication)
   {
      if (Settings.ExceptionSettings.Logging.LoggingMethod ==
          LoggingMethod.Database)
      {
         Logger.LogToDatabase(errorData);
      }
      else if (Settings.ExceptionSettings.Logging.LoggingMethod ==
               LoggingMethod.Xml)
      {
         Logger.LogToXmlLog(errorData);
      }
   }

   if (Settings.ExceptionSettings.Logging.EventLoggingEnabled &&
       logToWindowsEventLog)
   {
      Logger.LogToEventLog(errorData);
   }
}

In Listing 29, you'll see a couple of overloads of the LogAndNotify method, which provide convenient one-step access to all the error logging and notification features and configured options. The second overload lets you individually specify which features you want to use and automatically checks your configured options before performing its actions. So, for example, if you swap your configured data store from Xml to Database, using the LogAndNotify method would take care of logging to the proper data store automatically, and you wouldn't have to change any calls in your code.

Note that there's an even more compact way to call this method, provided by the first overload in Listing 29. The first overload just calls the second one while providing arguments of true for each Boolean parameter. Passing an ErrorData object to this method simply tells the program: "Here's an error. Send an e-mail if e-mail notification is enabled, send a text if that's enabled, log it to whichever data store is configured, then log it to the Event Log if that's enabled." Ninety-nine percent of the time, that's what you'd want to do anyway. In fact, if you refer back to Listing 22, you'll see this is the exact method used internally by Application_Error in the ErrorModule class. So you see, even though there are many powerful features at your disposal, you only need to know and use one short and simple method to leverage all of them automatically. Pretty cool, right?

1.6.3. Creating a Custom Error UI

Of course, none of this great stuff means a thing if you don't somehow get it up on screen. To make the magic happen, you need to do two things:

  • Create the custom error pages:

    • The error page to display when an unhandled exception occurs

    • A page or pages that will be displayed for any HTTP status codes you've chosen to pass off to the ASP.NET default exception handler

  • Build the administration pages

    • A page to show a list of errors from the log

    • A page that lets you examine details about a single error

1.6.3.1. Creating the Error Page

If you've played with the Fourth Coffee demo, you've already seen what the ErrorPage.aspx page looks like. Let's now take a moment to examine how it was built. As the page code and its associated code-behind are quite long, I won't reproduce it here, but I encourage you to open it yourself and follow along. If you study the code-behind, you should have no trouble understanding how it works.

You'll notice that the page is split into three basic sections: the top part of the page that is shown to all users, a panel that is shown only to normal users, and a panel that is shown only to privileged users. The Page_Load method shows or hides these panels according to the values obtained from the ExceptionSettings properties.

The btnSend_Click method is invoked by the user when posting back the user feedback form. It calls the GetErrorData method to retrieve the ErrorData object that was stored in the Application_Error handler. It then attempts to send the user's feedback, with the error details appended to the message.

Notice the exception handler here and how it's used. Here you catch SmtpException, then check to see if the user supplied her e-mail address. If she did, you know that she will be looking forward to a reply — so you should warn her not to expect one, because the mail didn't go through. However, if the user doesn't supply her e-mail address, that means she isn't expecting a reply, so in that case, the exception is "swallowed" and the user is made none the wiser. This is a design decision born of the fact that it may be somewhat frustrating to a user when your web site coughs up a second error in response to supplying feedback on the first one. You may or may not agree with this decision; of course, you are always free to implement something different.

The private DisplayExceptionDetails method, well ... displays exception details. This is called from Page_Load only if the current user is allowed to see detailed error reports. It also displays and populates a hyperlink to the logged error if logging is enabled, so that an even more detailed error report can be conveniently accessed.

Finally, note the page-level exception handler in Page_Error. This is one of those situations in which handling the exception in the page is extremely handy. Think about it — if this were omitted, any unhandled exception thrown from ErrorPage.aspx would float up to our custom handler, which would (you guessed it) try to show the error in ErrorPage.aspx! Although the runtime can usually detect and break out of an exception loop such as this, you still wouldn't be able to see what happened or easily debug the issue.

1.6.3.2. Pages for HTTP Errors

The Fourth Coffee site comes originally configured to ignore HTTP status codes 403 and 404, which as explained before, passes control of these errors to the ASP.NET default exception handler. If you do this, you should configure the default exception handler to handle the ignored errors, as in Listing 30.

Example 30. Configuring the ASP.NET default exception handler to handle HTTP errors
<customErrors mode="On" defaultRedirect="ErrorPages/ErrorPage.aspx">
   <error statusCode="404" redirect="ErrorPages/Http404ErrorPage.aspx"/>
   <error statusCode="403" redirect="ErrorPages/Http403ErrorPage.aspx"/>
</customErrors>

This then requires you to set up the additional error pages you need. Nothing strenuous here — HTTP error pages don't need to be fancy or elaborate. Remember, all the information you'll have available to you anyway at this point will be the error path held in the query string.

NOTE

While you're at it, you should also set the defaultRedirect attribute, preferably to the same custom error page you're already using. This is so that if an exception happens to be thrown from Application_Error (in other words, if the unhandled exception handler throws its own unhandled exception!), control over the original exception would pass to the ASP.NET default handler. By specifying defaultRedirect, you can use the default handler as a backup for your custom handler in case it fails.

1.6.3.3. The Admin Pages

The cherry on top of this sundae is the pages that let you browse the error logs. After all, collecting data is a waste of time if you don't have a good way to browse and manage it!

You build an administrative dashboard as you would any set of data-driven pages. For these purposes, you would use the classes in the LD.ExceptionHandling.Logging namespace to select and delete error records from the data store.

If you recall, the Fourth Coffee site provides links to three different log viewing implementations under the Admin menu. The Database Log page works with error records logged into the database. The Xml Log works with records logged into a text file in XML format. And the Consolidated Log uses data methods that work across both the database and XML file. You'll see this right away if you check out the methods used by the ObjectDataSource controls in these pages. Of course, keep in mind that this is a site built to demonstrate functionality. In your own sites, you likely wouldn't need or want to use all three of these formats.

This begs an interesting question — which data store should you use in practice?

If yours is a data-driven site, the logical choice would be to log errors to a database, and use the methods of LD.ExceptionHandling.Logging.DatabaseLog to build your error administration pages. Since database transactions are generally faster than disk-based file I/O, this would be the best-performing option. Note that these methods support server-side sorting and paging as well.

For sites that don't use a database, or if you'd rather not (or can't) add the required table to the database, you can log error data to an XML file. This offers the convenience of a text file, while still being queryable — the XML-based data access methods offer the same select, insert, and delete functionality that you would have with a database. In this case, you'd use the LD.ExceptionHandling.Logging.XmlLog methods for your data sources. Since the concept of server-side operations doesn't really apply to file storage, you are limited to client-side sorting and paging here. In practice, this shouldn't be a real problem, as you hopefully won't have a lot of error records once your site goes live!

The LD.ExceptionHandling.Logging.ConsolidatedLog class offers the same methods, but they work across both the Database and Xml logs simultaneously. For example, you can see that the ConsolidatedLog.GetErrors method merges the results of the GetErrors methods of the other two classes. If you have records in both logs, you can use these methods to bring them together, essentially treating them as a single data store. In a real site, you would probably want to pick one method or the other and stick with it; but for the purposes of this sample site, these consolidated methods let you conveniently experiment by switching back and forth between Database and Xml logging, while being able to browse, view, and manage errors all in one place.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.151.44