Chapter 7. Error Handling, Logging, and Tracing

No software can run free from error, and ASP.NET applications are no exception. Sooner or later your code will be interrupted by a programming mistake, invalid data, unexpected circumstances, or even hardware failure. Novice programmers spend sleepless nights worrying about errors. Professional developers recognize that bugs are an inherent part of software applications and code defensively, testing assumptions, logging problems, and writing error handling code to deal with the unexpected.

In this chapter, you'll learn the error handling and debugging practices that you can use to defend your ASP.NET applications against common errors, track user problems, and solve mysterious issues. You'll learn how to use structured exception handling, how to keep a record of unrecoverable errors with logs, and how to set up web pages with custom error messages for common HTTP errors. You'll also learn how to use page tracing to see diagnostic information about ASP.NET pages.

Common Errors

Errors can occur in a variety of situations. Some of the most common causes of errors include attempts to divide by zero (usually caused by invalid input or missing information) and attempts to connect to a limited resource such as a file or a database (which can fail if the file doesn't exist, the database connection times out, or the code has insufficient security credentials).

One infamous type of error is the null reference exception, which usually occurs when a program attempts to use an uninitialized object. As a .NET programmer, you'll quickly learn to recognize and resolve this common but annoying mistake. The following code example shows the problem in action, with two SqlConnection objects that represent database connections:

' Define a variable named conOne and create the object.
Private conOne As New SqlConnection()

' Define a variable named conTwo, but don't create it.
Private conTwo As SqlConnection

Protected Sub cmdDoSomething_Click(ByVal sender As Object, _
  ByVal e As EventArgs) Handles cmdCompute.Click

    ' This works, because the object has been created
    ' with the New keyword.
    conOne.ConnectionString = "..."
    ...
' The following statement will fail and generate a
    ' null reference exception.
    ' You cannot modify a property (or use a method) of an
    ' object that doesn't exist!
    conTwo.ConnectionString = "..."
    ...
End Sub

When an error occurs in your code, .NET checks to see whether any error handlers appear in the current scope. If the error occurs inside a method, .NET searches for local error handlers and then checks for any active error handlers in the calling code. If no error handlers are found, the page processing is aborted, and Visual Studio enters debug mode (as you learned in Chapter 2). If you press the Play button to keep going, you'll see a detailed error page that explains the problem (as shown in Figure 7-1). These error pages are a development convenience—once you deploy your application, they are replaced by more general error pages, which you can configure in the IIS web server software (as described in Chapter 26).

A sample error page

Figure 7.1. A sample error page

Even if an error is the result of invalid input or the failure of a third-party component, an error page can shatter the professional appearance of any application. The application users end up with a feeling that the application is unstable, insecure, or of poor quality—and they're at least partially correct.

If an ASP.NET application is carefully designed and constructed, an error page will almost never appear. Errors may still occur because of unforeseen circumstances, but they will be caught in the code and identified. If the error is a critical one that the application cannot solve on its own, it will report a more useful (and user-friendly) page of information that might include a link to a support e-mail or a phone number where the customer can receive additional assistance. You'll look at those techniques in this chapter.

Exception Handling

Most .NET languages support structured exception handling. Essentially, when an error occurs in your application, the .NET Framework creates an exception object that represents the problem. You can catch this object using an exception handler. If you fail to use an exception handler, your code will be aborted, and the user will see an error page.

Structured exception handling provides several key features:

Exceptions are object-based:

Each exception provides a significant amount of diagnostic information wrapped into a neat object, instead of a simple message and error code. These exception objects also support an InnerException property that allows you to wrap a generic error over the more specific error that caused it. You can even create and throw your own exception objects.

Exceptions are caught based on their type:

This allows you to streamline error handling code without needing to sift through obscure error codes.

Exception handlers use a modern block structure:

This makes it easy to activate and deactivate different error handlers for different sections of code and handle their errors individually.

Exception handlers are multilayered:

You can easily layer exception handlers on top of other exception handlers, some of which may check only for a specialized set of errors.

Exceptions are a generic part of the .NET Framework:

This means they're completely cross-language compatible. Thus, a .NET component written in C# can throw an exception that you can catch in a web page written in VB.

Note

Exception handlers are a key programming technique. They allow you to react to problems that occur at runtime because of factors outside your control. However, you obviously shouldn't use exception handlers to hide the bugs that might crop up in your code! Instead, you need to track down these programmer mistakes at development time and correct them. Visual Studio's debugging features (which were described in Chapter 4) can help you in this task.

The Exception Class

Every exception class derives from the base class System.Exception. The .NET Framework is full of predefined exception classes, such as NullReferenceException, IOException, SqlException, and so on. The Exception class includes the essential functionality for identifying any type of error. Table 7-1 lists its most important members.

Table 7.1. Exception Properties

Member

Description

HelpLink

A link to a help document, which canbe a relative or fully qualified uniform resource locator (URL) or uniform resource name (URN), such as file:///C:/ACME/MyApp/help.html#Err42. The .NET Framework doesn't use this property, but you can set it in your custom exceptions if you want to use it in your web page code.

InnerException

A nested exception. For example, a method might catch a simple file input/output (IO) error and create a higher-level "operation failed" error. The details about the original error could be retained in the InnerException property of the higher-level error.

Message

A text description with a significant amount of information describing the problem.

Source

The name of the application or object where the exception was raised.

StackTrace

A string that contains a list of all the current method calls on the stack, in order of most to least recent. This is useful for determining where the problem occurred.

TargetSite

A reflection object (an instance of the System.Reflection.MethodBase class) that provides some information about the method where the error occurred. This information includes generic method details such as the method name and the data types for its parameter and return values. It doesn't contain any information about the actual parameter values that were used when the problem occurred.

GetBaseException()

A method useful for nested exceptions that may have more than one layer. It retrieves the original (deepest nested) exception by moving to the base of the InnerException chain.

When you catch an exception in an ASP.NET page, it won't be an instance of the generic System.Exception class. Instead, it will be an object that represents a specific type of error. This object will be based on one of the many classes that inherit from System.Exception. These include diverse classes such as DivideByZeroException, ArithmeticException, IOException, SecurityException, and many more. Some of these classes provide additional details about the error in additional properties.

Visual Studio provides a useful tool to browse through the exceptions in the .NET class library. Simply select Debug

Exception Properties
Visual Studio's exception viewer

Figure 7.2. Visual Studio's exception viewer

The Exceptions dialog box allows you to specify what exceptions should be handled by your code when debugging and what exceptions will cause Visual Studio to enter break mode immediately. That means you don't need to disable your error handling code to troubleshoot a problem. For example, you could choose to allow your program to handle a common FileNotFoundException (which could be caused by an invalid user selection) but instruct Visual Studio to pause execution if an unexpected DivideByZero exception occurs.

To set this up, add a check mark in the Thrown column next to the entry for the System.DivideByZero exception. This way, you'll be alerted as soon as the problem occurs. If you don't add a check mark to the Thrown column, your code will continue, run any exception handlers it has defined, and try to deal with the problem. You'll be notified only if an error occurs and no suitable exception handler is available.

The Exception Chain

Figure 7-3 shows how the InnerException property works. In the specific scenario shown here, a FileNotFoundException led to a NullReferenceException, which led to a custom UpdateFailedException. Using an exception handling block, the application can catch the UpdateFailedException. It can then get more information about the source of the problem by following the InnerException property to the NullReferenceException, which in turn references the original FileNotFoundException.

Exceptions can be chained together.

Figure 7.3. Exceptions can be chained together.

The InnerException property is an extremely useful tool for component-based programming. Generally, it's not much help if a component reports a low-level problem such as a null reference or a divide-by-zero error. Instead, it needs to communicate a more detailed message about which operation failed and what input may have been invalid. The calling code can then often correct the problem and retry the operation.

On the other hand, sometimes you're debugging a bug that lurks deep inside the component itself. In this case, you need to know precisely what caused the error—you don't want to replace it with a higher-level exception that could obscure the root problem. Using an exception chain handles both these scenarios: you receive as many linked exception objects as needed, which can specify information from the least to the most specific error condition.

Handling Exceptions

The first line of defense in an application is to check for potential error conditions before performing an operation. For example, a program can explicitly check whether the divisor is 0 before performing a calculation or whether a file exists before attempting to open it:

If Divisor <> 0 Then
    ' Safe to divide some number by Divisor.
End If

If System.IO.File.Exists("myfile.txt") Then
    ' You can now open the myfile.txt file.
    ' However, you should still use exception handling because a variety of
    ' problems can intervene (insufficient rights, hardware failure, etc.).
End If

Even if you perform this basic level of "quality assurance," your application is still vulnerable. For example, you have no way to protect against all the possible file access problems that occur, including hardware failures or network problems that could arise spontaneously in the middle of an operation. Similarly, you have no way to validate a user ID and password for a database before attempting to open a connection—and even if you did, that technique would be subject to its own set of potential errors. In some cases, it may not be practical to perform the full range of defensive checks, because they may impose a noticeable performance drag on your application. For all these reasons, you need a way to detect and deal with errors when they occur.

The solution is structured exception handling. To use structured exception handling, you wrap potentially problematic code in the special block structure shown here:

Try
    ' Risky code goes here (opening a file, connecting to a database, and so on).
Catch
    ' An error has been detected. You can deal with it here.
Finally
    ' Time to clean up, regardless of whether or not there was an error.
End Try

The Try statement enables error handling. Any exceptions that occur in the following lines can be "caught" automatically. The code in the Catch block will be executed when an error is detected. And either way, whether a bug occurs or not, the Finally block of the code will be executed last. This allows you to perform some basic cleanup, such as closing a database connection. The Finally code is important because it will execute even if an error has occurred that will prevent the program from continuing. In other words, if an unrecoverable exception halts your application, you'll still have the chance to release resources.

The act of catching an exception neutralizes it. If all you want to do is render a specific error harmless, you don't even need to add any code in the Catch block of your error handler. Usually, however, this portion of the code will be used to report the error to the user or log it for future reference. In a separate component (such as a business object), this code might handle the exception, perform some cleanup, and then rethrow it to the calling code, which will be in the best position to remedy it or alert the user. Or it might actually create a new exception object with additional information and throw that.

Catching Specific Exceptions

Structured exception handling is particularly flexible because it allows you to catch specific types of exceptions. To do so, you add multiple Catch statements, each one identifying the type of exception (and providing a new variable to catch it in), as follows:

Try
    ' Database code goes here.
Catch err As System.Data.SqlClient.SqlException
    ' Catches common database problems like connection errors.
Catch err As System.NullReferenceException
    ' Catches problems resulting from an uninitialized object.
End Try

An exception will be caught as long as it's an instance of the indicated class or if it's derived from that class. In other words, if you use this statement:

Catch err As Exception

you will catch any exception, because every exception object is derived from the System.Exception base class.

Exception blocks work a little like conditional code. As soon as a matching exception handler is found, the appropriate Catch code is invoked. Therefore, you must organize your Catch statements from most specific to least specific:

Try
    ' Database code goes here.
Catch err As System.Data.SqlClient.SqlException
    ' Catches common database problems like connection errors.
Catch err As System.NullReferenceException
    ' Catches problems resulting from an uninitialized object.
Catch err As System.Exception
    ' Catches any other errors.
End Try

Ending with a Catch statement for the base Exception class is often a good idea to make sure no errors slip through. However, in component-based programming, you should make sure you intercept only those exceptions you can deal with or recover from. Otherwise, it's better to let the calling code catch the original error.

Nested Exception Handlers

When an exception is thrown, .NET tries to find a matching Catch statement in the current method. If the code isn't in a local structured exception block or if none of the Catch statements matches the exception, .NET will move up the call stack one level at a time, searching for active exception handlers.

Consider the example shown here, where the Page.Load event handler calls a private DivideNumbers() method:

Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As EventArgs) Handles Me.Load
    Try
        DivideNumbers(5, 0)
    Catch err As DivideByZeroException
        ' Report error here.
    End Try
End Sub
Private Function DivideNumbers(ByVal number As Decimal, _
  ByVal divisor As Decimal) As Decimal
    Return number/divisor
End Function

In this example, the DivideNumbers() method lacks any sort of exception handler. However, the DivideNumbers() method call is made inside a Try block, which means the problem will be caught further upstream in the calling code. This is a good approach because the DivideNumbers() routine could be used in a variety of circumstances (or if it's part of a component, in a variety of different types of applications). It really has no access to any kind of user interface and can't directly report an error. Only the calling code is in a position to determine whether the problem is a serious one or a minor one, and only the calling code can prompt the user for more information or report error details in the web page.

Note

In this example, great care is taken to use the Decimal data type rather than the more common Double data type. That's because contrary to what you might expect, it is acceptable to divide a Double by 0. The result is the special value Double.PositiveInfinity (or Double.NegativeInfinity if you divide a negative number by 0).

You can also overlap exception handlers in such a way that different exception handlers filter out different types of problems. Here's one such example:

Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As EventArgs) Handles Me.Load
    Try
        Dim Average As Integer = GetAverageCost(DateTime.Now)
    Catch err As DivideByZeroException
        ' Report error here.
    End Try
End Sub

Private Function GetAverageCost(saleDate As Date) As Integer
    Try
        ' Use Database access code here to retrieve all the sale records
        ' for this date, and calculate the average.
    Catch err As System.Data.SqlClient.SqlException
        ' Handle a database related problem.
    Finally
        ' Close the database connection.
    End Try
End Function

Dissecting the Code . . .

You should be aware of the following points:

  • If an SqlException occurs during the database operation, it will be caught in the GetAverageCost() method.

  • If a DivideByZeroException occurs (for example, the method receives no records but still attempts to calculate an average), the exception will be caught in the calling Page.Load event handler.

  • If another problem occurs (such as a null reference exception), no active exception handler exists to catch it. In this case, .NET will search through the entire call stack without finding a matching Catch statement in an active exception handler and will generate a runtime error, end the program, and return a page with exception information.

Exception Handling in Action

You can use a simple program to test exceptions and see what sort of information is retrieved. This program allows a user to enter two values and attempts to divide them. It then reports all the related exception information in the page (see Figure 7-4).

Catching and displaying exception information

Figure 7.4. Catching and displaying exception information

Obviously, you can easily prevent this exception from occurring by using extra code-safety checks, or you can elegantly resolve it using the validation controls. However, this code provides a good example of how you can deal with the properties of an exception object. It also gives you a good idea about what sort of information will be returned.

Here's the page class code for this example:

Public Partial Class ErrorHandlingTest
    Inherits System.Web.UI.Page

    Protected Sub cmdCompute_Click(ByVal sender As Object, _
      ByVal e As EventArgs) Handles cmdCompute.Click

        Try
            Dim A, B, Result As Decimal
            A = Decimal.Parse(txtA.Text)
            B = Decimal.Parse(txtB.Text)
            Result = A / B
            lblResult.Text = Result.ToString()
            lblResult.ForeColor = System.Drawing.Color.Black
        Catch err As Exception
            lblResult.Text = "<b>Message:</b> " & err.Message & "<br /><br />"
            lblResult.Text &= "<b>Source:</b> " & err.Source & "<br /><br />"
            lblResult.Text &= "<b>Stack Trace:</b> " & err.StackTrace
            lblResult.ForeColor = System.Drawing.Color.Red
        End Try
    End Sub

End Class

Note that as soon as the error occurs, execution is transferred to an exception handler. The code in the Try block isn't completed. It's for that reason that the result for the label is set in the Try block. These lines will be executed only if the division code runs error-free.

You'll see many more examples of exception handling throughout this book. The data access chapters in Part 4 of this book show the best practices for exception handling when accessing a database.

Mastering Exceptions

Keep in mind these points when working with structured exception handling:

Break down your code into multiple Try/Catch blocks:

If you put all your code into one exception handler, you'll have trouble determining where the problem occurred. You have no way to "resume" the code in a Try block. This means that if an error occurs at the beginning of a lengthy Try block, you'll skip a large amount of code. The rule of thumb is to use one exception handler for one related task (such as opening a file and retrieving information).

Report all errors:

During debugging, portions of your application's error handling code may mask easily correctable mistakes in your application. To prevent this from happening, make sure you report all errors, and consider leaving out some error handling logic in early builds.

Don't use exception handlers for every statement:

Simple code statements (assigning a constant value to a variable, interacting with a control, and so on) may cause errors during development testing but will not cause any future problems once perfected. Error handling should be used when you're accessing an outside resource or dealing with supplied data that you have no control over (and thus may be invalid).

Throwing Your Own Exceptions

You can also define your own exception objects to represent custom error conditions. All you need to do is create an instance of the appropriate exception class and then use the Throw statement.

The next example introduces a modified DivideNumbers() method. It explicitly checks whether the specified divisor is 0 and then manually creates and throws an instance of the DivideByZeroException class to indicate the problem, rather than attempt the operation. Depending on the code, this pattern can save time by eliminating some unnecessary steps, or it can prevent a task from being initiated if it can't be completed successfully.

Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As EventArgs) Handles Me.Load
    Try
        DivideNumbers(5, 0)
    Catch err As DivideByZeroException
        ' Report error here.
    End Try
End Sub

Private Function DivideNumbers(ByVal number As Decimal, _
  ByVal divisor As Decimal) As Decimal
    If divisor = 0 Then
        Dim err As New DivideByZeroException()
        Throw err
    Else
        Return number/divisor
    End If
End Function

Alternatively, you can create a .NET exception object and specify a custom error message by using a different constructor:

Private Function DivideNumbers(ByVal number As Decimal, _
  ByVal divisor As Decimal) As Decimal
    If divisor = 0 Then
        Dim err As New DivideByZeroException( _
           "You supplied 0 for the divisor parameter. You must be stopped.")
        Throw err
    Else
        Return number/divisor
    End If
End Function

In this case, any ordinary exception handler will still catch the DivideByZeroException. The only difference is that the error object has a modified Message property that contains the custom string. Figure 7-5 shows the resulting exception.

Standard exception, custom message

Figure 7.5. Standard exception, custom message

Throwing an exception is most useful in component-based programming. In component-based programming, your ASP.NET page is creating objects and calling methods from a class defined in a separately compiled assembly. In this case, the class in the component needs to be able to notify the calling code (the web application) of any errors. The component should handle recoverable errors quietly and not pass them up to the calling code. On the other hand, if an unrecoverable error occurs, it should always be indicated with an exception and never through another mechanism (such as a return code). For more information about component-based programming, refer to Chapter 22.

If you can find an exception in the class library that accurately reflects the problem that has occurred, you should throw it. If you need to return additional or specialized information, you can create your own custom exception class.

Custom exception classes should always inherit from System.ApplicationException, which itself derives from the base Exception class. This allows .NET to distinguish between two broad classes of exceptions—those you create and those that are native to the .NET Framework.

When you create an exception class, you can add properties to record additional information. For example, here is a special class that records information about the failed attempt to divide by zero:

Public Class CustomDivideByZeroException
  Inherits ApplicationException

    ' Add a variable to specify the "other" number.
    ' Depending on the circumstance, this might help diagnose the problem.
    Public DividingNumber As Decimal

End Class

Note

You can define the custom exception class directly in your .aspx.vb code file. However, a more organized approach is to use a separate code file. To do this, right-click your application in the Solution Explorer, and choose Add New Item. Then, pick Class from the list of templates, enter a file name, and click Add. Visual Studio will place a new code file in the App_Code subfolder of your website. (It's up to you whether you create a separate code file for every class or put several classes in the same code file. Both approaches have the same effect.)

You can throw this custom exception like this:

Private Function DivideNumbers(ByVal number As Decimal, _
  ByVal divisor As Decimal) As Decimal
    If divisor = 0 Then
        Dim err As New CustomDivideByZeroException()
        err.DividingNumber = number
        Throw err
    Else
        Return number/divisor
    End If
End Function

To perfect the custom exception, you need to supply it with the three standard constructors. This allows your exception class to be created in the standard ways that every exception supports:

  • On its own, with no arguments

  • With a custom message

  • With a custom message and an exception object to use as the inner exception

These constructors don't actually need to contain any code. All these constructors need to do is forward the parameters to the base class (the constructors in the inherited ApplicationException class) using the MyBase keyword, as shown here:

Public Class CustomDivideByZeroException
  Inherits ApplicationException

    ' Add a variable to specify the "other" number.
    ' Depending on the circumstance, this might help diagnose the problem.
    Private _dividingNumber As Decimal
    Public Property DividingNumber() As Decimal
        Get
            Return _dividingNumber
        End Get
        Set(ByVal value As Decimal)
            _dividingNumber = value
        End Set
    End Property

    Public Sub New()
        MyBase.New()
    End Sub
Public Sub New(ByVal message As String)
        MyBase.New(message)
    End Sub

    Public Sub New(ByVal message as String, ByVal inner As Exception)
        MyBase.New(message, inner)
    End Sub

End Class

The third constructor is particularly useful for component programming. It allows you to set the InnerException property with the exception object that caused the original problem. The next example shows how you could use this constructor with a component class called ArithmeticUtility:

Public Class ArithmeticUtilityException
  Inherits ApplicationException
    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(ByVal message As String)
        MyBase.New(message)
    End Sub

    Public Sub New(ByVal message as String, ByVal inner As Exception)
        MyBase.New(message, inner)
    End Sub
End Class

Public Class ArithmeticUtility
    Private Function Divide(ByVal number As Decimal, _
      ByVal divisor As Decimal) As Decimal
        Try
            Return number/divisor
        Catch err As Exception
            ' Create an instance of the specialized exception class,
            ' and place the original exception object (for example, a
            ' DivideByZeroException) in the InnerException property.
            Dim errNew As New ArithmeticUtilityException("Calculation error", _
              err)

            ' Now throw the new exception.
            Throw errNew
        End Try
    End Function
End Class

Remember, custom exception classes are really just a standardized way for one class to communicate an error to a different portion of code. If you aren't using components or your own utility classes, you probably don't need to create custom exception classes.

Logging Exceptions

In many cases, it's best not only to detect and catch exceptions but to log them as well. For example, some problems may occur only when your web server is dealing with a particularly large load. Other problems might recur intermittently, with no obvious causes. To diagnose these errors and build a larger picture of site problems, you need to log exceptions so they can be reviewed later.

The .NET Framework provides a wide range of logging tools. When certain errors occur, you can send an e-mail, add a database record, or write to a file. Many of these techniques are described in other parts of this book.

Tip

In many cases, it makes sense to use more than one logging technique. For example, you may decide to log the majority of your errors to an ErrorLog table in a database but log database errors to another place, such as the Windows event log.

One of the most fail-safe logging tools is the Windows event logging system, which is built into the Windows operating system and available to any application. Using the Windows event logs, your website can write text messages that record errors or unusual events. The Windows event logs store your messages as well as various other details, such as the message type (information, error, and so on) and the time the message was left.

Viewing the Windows Event Logs

To view the Windows event logs, you use the Event Viewer tool that's included with Windows. To launch it, begin by selecting Start

Viewing the Windows Event Logs

Table 7.2. Windows Event Logs

Log Name

Description

Application

Used to track errors or notifications from any application. Generally, you'll use this log when you're performing event logging, or you'll create your own custom log.

Security

Used to track security-related problems but generally used exclusively by the operating system.

System

Used to track operating system events.

Setup

Used to track issues that occur when installing Windows updates or other software.

Using the Event Viewer, you can perform a variety of management tasks with the logs. For example, if you right-click one of the logs in the Event Viewer list you'll see options that allow you to clear the events in the log, save the log entries to another file, and import an external log file.

Each event record in an event log identifies the source (generally, the application or service that created the record), the type of notification (error, information, warning), and the time the log entry was inserted. To see this information, you simply need to select a log entry, and the details will appear in a display area underneath the list of entries (see Figure 7-6).

The Event Viewer

Figure 7.6. The Event Viewer

You can also review event logs in Visual Studio, unless you're running the slightly less powerful Visual Studio Web Developer Express edition, which doesn't include this feature. First, display the Server Explorer window (if it's not already visible) by choosing View

The Event Viewer

If you expand an event log in the Server Explorer window, you'll find all the event log entries, grouped according to the source that made the log entry. Figure 7-7 shows some of the event logs left in the Application log on the current computer by the event source .NET Runtime Optimization Source. Once you select a log entry, you can view its specific details (such as the event log message and the time it was left) in the Properties window.

Viewing event log entries in Visual Studio

Figure 7.7. Viewing event log entries in Visual Studio

One of the potential problems with event logs is that old entries are automatically discarded when the event log reaches a maximum size (by default, 20MB). However, you'll find that logs grow quickly. That means that unless you're using a custom event log that has lots of space, your log entries might not last for a long period of time. Ideally, you should use event logs to record information that is reviewed and acted on over a relatively short period of time. For example, event logs are a good choice if you plan to log application errors and review them to diagnose strange behavior immediately after it happens. Event logs don't make as much sense if you want to get a detailed picture of application activity six months later, because Windows (or someone else) may delete old log entries. In this scenario, a custom database makes more sense.

If you want to add a little more breathing room to an existing log, you can change its maximum size. To do so, right-click the log and choose Properties. You'll see the Application Properties window shown in Figure 7-8, where you can change the maximum size.

Log properties

Figure 7.8. Log properties

Tip

You can increase the log size, but you really shouldn't disable automatic log deletion altogether, because you could end up consuming a huge amount of space over time if information isn't being regularly removed.

Writing to the Event Log

You can interact with event logs in an ASP.NET page by using the classes in the System.Diagnostics namespace. First, import the namespace at the beginning of your code-behind file:

Imports System.Diagnostics

The following example rewrites the simple ErrorTest page to use event logging:

Public Partial Class ErrorTestLog
    Inherits System.Web.UI.Page

    Protected Sub cmdCompute_Click(ByVal sender As Object, _
      ByVal e As EventArgs) _
      Handles cmdCompute.Click
Try
            Dim A, B, Result As Decimal
            A = Decimal.Parse(txtA.Text)
            B = Decimal.Parse(txtB.Text)
            Result = A / B
            lblResult.Text = Result.ToString()
            lblResult.ForeColor = System.Drawing.Color.Black
        Catch err As Exception
            lblResult.Text = "<b>Message:</b> " & err.Message & "<br /><br />"
            lblResult.Text &= "<b>Source:</b> " & err.Source & "<br /><br />"
            lblResult.Text &= "<b>Stack Trace:</b> " & err.StackTrace
            lblResult.ForeColor = System.Drawing.Color.Red

            ' Write the information to the event log.
            Dim Log As New EventLog()
            Log.Source = "DivisionPage"
            Log.WriteEntry(err.Message, EventLogEntryType.Error)
        End Try
    End Sub

End Class

The event log record will now appear in the Event Viewer utility, as shown in Figure 7-9. Note that logging is intended for the system administrator or developer. It doesn't replace the code you use to notify the user and explain that a problem has occurred.

An event record

Figure 7.9. An event record

Custom Logs

You can also log errors to a custom log. For example, you could create a log with your company name and add records to it for all your ASP.NET applications. You might even want to create an individual log for a particularly large application and use the Source property of each entry to indicate the page (or web service method) that caused the problem.

Accessing a custom log is easy—you just need to use a different constructor for the EventLog class to specify the custom log name. You also need to register an event source for the log. This initial step needs to be performed only once—in fact, you'll receive an error if you try to create the same event source. Typically, you'll use the name of the application as the event source.

Here's an example that uses a custom log named ProseTech and registers the event source DivideByZeroApp:

' Register the event source if needed.
If Not EventLog.SourceExists("DivideByZeroApp") Then
    ' This registers the event source and creates the custom log,
    ' if needed.
    EventLog.CreateEventSource("DivideByZeroApp", "ProseTech")
End If

' Open the log. If the log does not exist, it will be created automatically.
Dim Log As New EventLog("ProseTech")
log.Source = "DivideByZeroApp"
log.WriteEntry(err.Message, EventLogEntryType.Error)

If you specify the name of a log that doesn't exist when you use the CreateEventSource() method, the system will create a new, custom event log for you the first time you write an entry.

To see a newly created event log in the Event Viewer tool, you'll need to exit Event Viewer and restart it. Custom logs appear in a separate group named Applications and Services Logs, as shown in Figure 7-10.

A custom log

Figure 7.10. A custom log

You can use this sort of code anywhere in your application. Usually, you'll use logging code when responding to an exception that might be a symptom of a deeper problem.

A Custom Logging Class

Rather than adding too much logging code in the Catch block, a better approach is to create a separate class that handles the event logging grunt work. You can then use that class from any web page, without duplicating any code.

To use this approach, begin by creating a new code file in the App_Code subfolder of your website. You can do this in Visual Studio by choosing Website

A Custom Logging Class

Here's an example of a class named MyLogger that handles the event logging details:

Public Class MyLogger
    Public Sub LogError(ByVal pageInError As String, ByVal err As Exception)
        RegisterLog()

        Dim log As New EventLog("ProseTech")
        log.Source = pageInError
        log.WriteEntry(err.Message, EventLogEntryType.Error)
    End Sub

    Private Sub RegisterLog()
        ' Register the event source if needed.
        If Not EventLog.SourceExists("ProseTech") Then
           EventLog.CreateEventSource("DivideByZeroApp", "ProseTech")
        End If
    End Sub
End Class

Once you have a class in the App_Code folder, it's easy to use it anywhere in your website. Here's how you might use the MyLogger class in a web page to log an exception:

Try
    ' Risky code goes here.

Catch err As Exception
    ' Log the error using the logging class.
    Dim logger As New MyLogger()
    logger.LogError(Request.Path, err)

    ' Now handle the error as appropriate for this page.
    lblResult.Text = "Sorry. An error occurred."
Emd Try

If you write log entries frequently, you may not want to check whether the log exists every time you want to write an entry. Instead, you could create the event source once—when the application first starts up—using an application event handler in the global.asax file. This technique is described in Chapter 5.

Tip

Event logging uses disk space and takes processor time away from web applications. Don't store unimportant information, large quantities of data, or information that would be better off in another type of storage (such as a relational database). Generally, you should use an event log to log unexpected conditions or errors, not customer actions or performance-tracking information.

Retrieving Log Information

One of the disadvantages of the event logs is that they're tied to the web server. This can make it difficult to review log entries if you don't have a way to access the server (although you can read them from another computer on the same network). This problem has several possible solutions. One interesting technique involves using a special administration page. This ASP.NET page can use the EventLog class to retrieve and display all the information from the event log.

Figure 7-11 shows in a simple web page all the entries that were left by the ErrorTestCustomLog page. The results are shown using a label in a scrollable panel (a Panel control with the Scrollbars property set to Vertical). A more sophisticated approach would use similar code but with one of the data controls discussed in Chapter 16.

A log viewer page

Figure 7.11. A log viewer page

Here's the web page code you'll need:

Public Partial Class EventReviewPage
    Inherits System.Web.UI.Page

    Protected Sub cmdGet_Click(ByVal sender As Object, _
      ByVal e As EventArgs) Handles cmdGet.Click

        lblResult.Text = ""

        ' Check if the log exists.
        If Not EventLog.Exists(txtLog.Text) Then
            lblResult.Text = "The event log " & txtLog.Text & _
              " does not exist."
        Else
            Dim log As New EventLog(txtLog.Text)

            For Each entry As EventLogEntry In log.Entries
                ' Write the event entries to the page.
                If chkAll.Checked Or entry.Source = txtSource.Text Then
                    lblResult.Text &= "<b>Entry Type:</b> "
                    lblResult.Text &= entry.EntryType.ToString()
                    lblResult.Text &= "<br /><b>Message:</b> " & entry.Message
                    lblResult.Text &= "<br /><b>Time Generated:</b> "
                    lblResult.Text &= entry.TimeGenerated
                    lblResult.Text &= "<br /><br />"
                End If
            Next
        End If
    End Sub

    Protected Sub chkAll_CheckedChanged(ByVal sender As Object, _
      ByVal e As EventArgs) Handles chkAll.CheckedChanged

        ' The chkAll control has AutoPostback = True.
        If chkAll.Checked Then
            txtSource.Text = ""
            txtSource.Enabled = False
        Else
            txtSource.Enabled = True
        End If
    End Sub

End Class

If you choose to display all the entries from the application log, the page will perform slowly. Two factors are at work here. First, it takes time to retrieve each event log entry; a typical application log can easily hold several thousand entries. Second, the code used to append text to the Label control is inefficient. Every time you add a new piece of information to the Label.Text property, .NET needs to generate a new String object. A better solution is to use the specialized System.Text.StringBuilder class, which is designed to handle intensive string processing with a lower overhead by managing an internal buffer or memory.

Here's the more efficient way you could write the string processing code:

' For maximum performance, join all the event
' information into one large string using the
' StringBuilder.
Dim sb As New System.Text.StringBuilder()

Dim log As New EventLog(txtLog.Text)

For Each entry As EventLogEntry In log.Entries
    ' Write the event entries to the StringBuilder.
    If chkAll.Checked Or entry.Source = txtSource.Text Then
        sb.Append("<b>Entry Type:</b> ")
        sb.Append(entry.EntryType.ToString())
        sb.Append("<br /><b>Message:</b> ")
        sb.Append(entry.Message)
        sb.Append("<br /><b>Time Generated:</b> ")
        sb.Append(entry.TimeGenerated.ToString())
        sb.Append("<br /><br />")
    End If
Next

' Copy the complete text to the web page.
lblResult.Text = sb.ToString()

Tip

You can get around some of the limitations of the event log by using your own custom logging system. All the ingredients you need are built into the common class library. For example, you could store error information in a database using the data access techniques described in Chapter 14 .

Page Tracing

Visual Studio's debugging tools and ASP.NET's detailed error pages are extremely helpful when you're testing an application. However, sometimes you need a way to identify problems after you've deployed an application, when you don't have Visual Studio to rely on.

You could try to identify these errors by recording diagnostic information in an event log, but this assumes that someone will actually review the log regularly. More aggressively, you could display some information directly in the web page. The problem with this strategy is that you need to remove (or at least comment out) all this extra code before you deploy your web application. Otherwise, your website users could see strange debugging messages when they least expect it.

Fortunately, there's an easier way to solve the problem without resorting to a homegrown solution. ASP.NET provides a feature called tracing that gives you a far more convenient and flexible way to report diagnostic information.

Enabling Tracing

To use tracing, you need to explicitly enable it. There are several ways to switch on tracing. One of the easiest ways is by adding an attribute to the Page directive in the .aspx file:

<%@ Page Trace="True" ... %>

You can also enable tracing using the built-in Trace object (which is an instance of the System.Web.TraceContext class). Here's an example of how you might turn tracing on in the Page.Load event handler:

Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As EventArgs) Handles Me.Load
    Trace.IsEnabled = True
End Sub

This technique is useful because it allows you to enable or disable tracing for a page under specific circumstances that you test for in your code.

Note that by default, once you enable tracing, it will apply only to local requests. (In other words, if you're working with a deployed web application, you need to make your requests from the web browser on the web server computer.) This limitation prevents end users from seeing the tracing information. If you need to trace a web page from an offsite location, you'll need to enable remote tracing by changing some settings in the web.config file. (You can find information about modifying these settings in the "Application-Level Tracing" section later in this chapter.) Once you've enabled remote tracing, you can use your code to selectively turn on tracing—for example, for specific users.

Tracing Information

ASP.NET tracing automatically provides a lengthy set of standard, formatted information. Figure 7-12 shows what this information looks like. To build this example, you can start with any basic ASP.NET page. Shown here is a rudimentary ASP.NET page with just a label and a button.

A simple ASP.NET page

Figure 7.12. A simple ASP.NET page

On its own, this page does very little, displaying a single line of text. However, if you click the button, tracing is enabled by setting the Trace.IsEnabled property to True (as shown in the previous code snippet). When the page is rendered, it will include a significant amount of diagnostic information, as shown in Figure 7-13.

Tracing the simple ASP.NET page

Figure 7.13. Tracing the simple ASP.NET page

Tracing information is provided in several different categories, which are described in the following sections. Depending on your page, you may not see all the tracing information. For example, if the page request doesn't supply any query string parameters, you won't see the QueryString collection. Similarly, if there's no data being held in application or session state, you won't see those sections either.

Tip

If you're using style sheets, your rules may affect the formatting and layout of the trace information, potentially making it difficult to read. If this becomes a problem, you can use application-level tracing, as described later in this chapter (see the "Application-Level Tracing" section).

Request Details

This section includes some basic information such as the current session ID, the time the web request was made, and the type of web request and encoding (see Figure 7-14). Most of these details are fairly uninteresting, and you won't spend much time looking at them. The exception is the session ID. By watching for when the session ID changes, you'll know when a new session is created. (Sessions are used to store information for a specific user in between page requests. You'll learn about them in Chapter 8.)

Request details

Figure 7.14. Request details

Trace Information

Trace information shows the different stages of processing that the page went through before being sent to the client (see Figure 7-15). Each section has additional information about how long it took to complete, as a measure from the start of the first stage (From First) and as a measure from the start of the previous stage (From Last). If you add your own trace messages (a technique described shortly), they will also appear in this section.

Trace information

Figure 7.15. Trace information

Control Tree

The control tree shows you all the controls on the page, indented to show their hierarchy (which controls are contained inside other controls), as shown in Figure 7-16. In this simple page example, the control tree includes buttons named cmdWrite, cmdWrite_Category, cmdError, and cmdSession, all of which are explicitly defined in the web page markup. ASP.NET also adds literal controls automatically to represent spacing and any other static elements that aren't server controls (such as text or ordinary HTML tags). These controls appear in between the buttons in this example, and have automatically generated names like ctl00, ctl01, ctl02, and so on.

One useful feature of this section is the Viewstate column, which tells you how many bytes of space are required to persist the current information in the control. This can help you gauge whether enabling control state is detracting from performance, particularly when working with data-bound controls such as the GridView.

Control tree

Figure 7.16. Control tree

Session State and Application State

These sections display every item that is in the current session or application state. Each item in the appropriate state collection is listed with its name, type, and value. If you're storing simple pieces of string information, the value is straightforward—it's the actual text in the string. If you're storing an object, .NET calls the object's ToString() method to get an appropriate string representation. For complex objects that don't override ToString() to provide anything useful, the result may just be the class name.

Figure 7-17 shows the session state section after you've added two items to session state (an ordinary string and a DataSet object). Chapter 8 has more about using session state.

Session state

Figure 7.17. Session state

Request Cookies and Response Cookies

These sections display the cookies that were sent by the web browser with the request for this page and display the cookies that were returned by the web server with the response. ASP.NET shows the content and the size of each cookie in bytes.

Figure 7-18 shows an example with a page that uses a cookie named Preferences that stores a single piece of information: a user name. (You learned to write the code that creates this cookie in Chapter 8.) In addition, the web browser receives a cookie named ASP.NET_SessionId, which ASP.NET creates automatically to store the current session ID.

Cookies collections

Figure 7.18. Cookies collections

There's one quirk with the list of cookies in the trace information. If you haven't created at least one custom cookie of your own, you won't see any cookies, including the ones that ASP.NET creates automatically (such as the session cookie). ASP.NET assumes that if you aren't using cookies yourself, you aren't interested in seeing these details.

Headers Collection

This section lists all the HTTP headers (see Figure 7-19). Technically, the headers are bits of information that are sent to the server as part of a request. They include information about the browser making the request, the types of content it supports, and the language it uses. In addition, the Response Headers Collection lists the headers that are sent to the client as part of a response (just before the actual HTML that's shown in the browser). The set of response headers is smaller, and it includes details such as the version of ASP.NET and the type of content that's being sent (text/html for web pages).

Generally, you don't need to use the header information directly. Instead, ASP.NET takes this information into account automatically.

Headers collection

Figure 7.19. Headers collection

Form Collection

This section lists the posted-back form information. The form information includes all the values that are submitted by web controls, like the text in a text box and the current selection in a list box. The ASP.NET web controls pull the information they need out of the form collection automatically, so you rarely need to worry about it.

Figure 7-20 shows the form values for the simple page shown in Figure 7-12. It includes the hidden view state field, another hidden field that's used for event validation (a low-level ASP.NET feature that helps prevent people from tampering with your web pages before posting them back), and a field for the cmdTrace button, which is the only web control on the page.

Form collection

Figure 7.20. Form collection

Query String Collection

This section lists the variables and values submitted in the query string. You can see this information directly in the web page URL (in the address box in the browser). However, if the query string consists of several different values and contains a lot of information, it may be easier to review the individual items in the trace display.

Figure 7-21 shows the information for a page that was requested with two query string values, one named search and the other named style. You can try this with the SimpleTrace.aspx page by typing in ?search=cat&style=full at the end of the URL in the address box of your web browser.

Query string collection

Figure 7.21. Query string collection

Server Variables

This section lists all the server variables and their contents. You don't generally need to examine this information. Note also that if you want to examine a server variable programmatically, you can do so by name with the built-in Request.ServerVariables collection or by using one of the more useful higher-level properties from the Request object.

Writing Trace Information

The default trace log provides a set of important information that can allow you to monitor some important aspects of your application, such as the current state contents and the time taken to execute portions of code. In addition, you'll often want to generate your own tracing messages. For example, you might want to output the value of a variable at various points in execution so you can compare it with an expected value. Similarly, you might want to output messages when the code reaches certain points in execution so you can verify that various procedures are being used (and are used in the order you expect). Once again, these are tasks you can also achieve using Visual Studio debugging, but tracing is an invaluable technique when you're working with a web application that's been deployed to a web server.

To write a custom trace message, you use the Write() method or the Warn() method of the built-in Trace object. These methods are equivalent. The only difference is that Warn() displays the message in red lettering, which makes it easier to distinguish from other messages in the list. Here's a code snippet that writes a trace message when the user clicks a button:

Protected Sub cmdWrite_Click(ByVal sender As Object, _
  ByVal e As EventArgs) Handles cmdWrite.Click

    Trace.Write("About to place an item in session state.")
    Session("Test") = "Contents"
    Trace.Write("Placed item in session state.")
End Sub

These messages appear in the trace information section of the page, along with the default messages that ASP.NET generates automatically (see Figure 7-22).

Custom trace messages

Figure 7.22. Custom trace messages

You can also use an overloaded method of Write() or Warn() that allows you to specify the category. A common use of this field is to indicate the current method, as shown in Figure 7-23.

A categorized trace message

Figure 7.23. A categorized trace message

Protected Sub cmdWriteCategory_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles cmdWriteCategory.Click

    Trace.Write("cmdWriteCategory_Click", _
      "About to place an item in session state.")
    Session("Test") = "Contents"
    Trace.Write("cmdWriteCategory_Click", _
      "Placed item in session state.")
End Sub

Alternatively, you can supply category and message information with an exception object that will automatically be described in the trace log, as shown in Figure 7-24.

An exception trace message

Figure 7.24. An exception trace message

Protected Sub cmdError_Click(ByVal sender As Object, _
  ByVal e As EventArgs) Handles cmdError.Click

    Try
        DivideNumbers(5, 0)
    Catch err As Exception
        Trace.Warn("cmdError_Click", "Caught Error", err)
    End Try
End Sub

Private Function DivideNumbers(ByVal number As Decimal, _
  ByVal divisor As Decimal) As Decimal
    Return number/divisor
End Sub

By default, trace messages are listed in the order they were written by your code. Alternatively, you can specify that messages should be sorted by category using the TraceMode attribute in the Page directive:

<%@ Page Trace="True" TraceMode="SortByCategory" %>

or the TraceMode property of the Trace object in your code:

Trace.TraceMode = TraceMode.SortByCategory

Application-Level Tracing

Application-level tracing allows you to enable tracing for an entire application. However, the tracing information won't be displayed in the page. Instead, it will be collected and stored in memory for a short amount of time. You can review the recently traced information by requesting a special URL. Application-level tracing provides several advantages. The tracing information won't be mangled by the formatting and layout in your web page, you can compare trace information from different requests, and you can review the trace information that's recorded for someone else's request.

To enable application-level tracing, you need to modify settings in the web.config file, as shown here:

<configuration>
  <system.web>
    <trace enabled="true" requestLimit="10" pageOutput="false" />
  </system.web>
</configuration>

Table 7-4 lists the tracing options.

Table 7.3. Tracing Options

Attribute

Values

Description

enabled

true, false

Turns application-level tracing on or off.

requestLimit

Any integer (for example, 10)

Stores tracing information for a maximum number of HTTP requests. Unlike page-level tracing, this allows you to collect a batch of information from multiple requests. When the maximum is reached, ASP.NET may discard the information from the oldest request (which is the default behavior) or the information from the new request, depending on the mostRecent setting.

pageOutput

true, false

Determines whether tracing information will be displayed on the page (as it is with page-level tracing). If you choose false, you'll still be able to view the collected information by requesting trace.axd from the virtual directory where your application is running.

traceMode

SortByTime, SortByCategory

Determines the sort order of trace messages. The default is SortByTime.

localOnly

true, false

Determines whether tracing information will be shown only to local clients (clients using the same computer) or can be shown to remote clients as well. By default, this is true and remote clients cannot see tracing information.

mostRecent

true, false

Keeps only the most recent trace messages if true. When the requestLimit maximum is reached, the information for the oldest request is abandoned every time a new request is received. If false (the default), ASP.NET stops collecting new trace messages when the limit is reached.

To view tracing information, you request the trace.axd file in the web application's root directory. This file doesn't actually exist; instead, ASP.NET automatically intercepts the request and interprets it as a request for the tracing information. It will then list the most recent collected requests, provided you're making the request from the local machine or have enabled remote tracing (see Figure 7-25).

Traced application requests

Figure 7.25. Traced application requests

Note

Sometimes when requesting trace.axd from Visual Studio's test web server, you may not see the most recent traced requests. In this situation, click the browser's Refresh button to get an updated list.

You can see the detailed information for any request by clicking the View Details link. This provides a useful way to store tracing information for a short period of time and allows you to review it without needing to see the actual pages (see Figure 7-26).

One of the traced application requests

Figure 7.26. One of the traced application requests

The Last Word

One of the most significant differences between an ordinary website and a professional web application is often in how it deals with errors. In this chapter, you learned the different lines of defense you can use in .NET, including structured error handling, logging, and tracing.

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

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