AutomatedQA: Award-winning tools for software development and quality assurance

Home » Technical Papers » Debugging in .NET

Debugging in .NET

Debugging is one of the most painful parts of the entire software development process. We don't want to describe how hard it can be to find one little bug - you probably all know this already... possibly, too well. The amount of bugs in programs increases with product complexity and often when bugs aren't fixed in time, they can interact and produce extra complexities in a project that is complex itself. So, we are trying to constantly monitor and eliminate bugs.

The best way to monitor bugs is to perform unit testing but, when it says that a test has failed, it doesn't say why and our task is to trace the execution process to see what's wrong.

In this article we just want to describe how this process can be simplified when using the .NET Framework debugging and tracing means. We'll briefly tell you how to use them efficiently, plus we'll you give some examples of this.


Tracing

The first debugging strategy to be discussed in this article is tracing. Tracing can be a very powerful technique in skillful hands, because it allows you to see the whole picture of the application run time behavior and analyze it more efficiently, although it cannot provide information by request.

Tracing support in .NET is covered (as most of the other debug components) in the System.Diagnostics namespace, exactly in the Trace class.

Trace is a static class (which means that all its members are static and you don't need to instantiate it to get the key to its functionality). It allows you to do the following:

Raise an assertion (conditional or not)
Output trace messages with condition support
Format the trace output

Let's start discovering its possibilities with a brief member's overview:

public static void Assert(bool)
public static void Assert(bool, string)
public static void Assert(bool, string, string)

The Assert method displays a failure message (it is displayed on the application failure and allows users to abort execution, ignore the failure or retry the code that caused the failure) if the condition is false. Two overloads allow displaying custom information by specifying one or two accompanying messages (these messages can be set by the string parameters - this is done to allow developers to display extra information about the assertion fail).

To describe how the messages are displayed, we'll demonstrate several examples to you. The first shows the results of assertion without any extra messages. Such an assertion can be made using code like the following:

// Assertion demo
// 
// Purpose: To demonstrate results of different Assert method calls

using System;
using System.Diagnostics;

namespace Assertion
{
    class Application
    {
        [STAThread]
        static void Main(string[] args)
        {
            // Simple assertion. No additional messages
            Trace.Assert(false);
        }
    }
}

This program displays the following message box:

As you can see, only exception information is displayed. This message doesn't display any context-dependent information and we can't see the reason of the failure. Next, the application uses one attribute when calling the Assert method:

// Assertion demo
// 
// Purpose: To demonstrate results of different Assert method calls

using System;
using System.Diagnostics;

namespace Assertion
{
    class Application
    {
        [STAThread]
        static void Main(string[] args)
        {
            // Assertion with one additional message.
            Trace.Assert(false, "Simple assertion message");
        }
    }
}

This program displays a message box with more information. You can see that now we can provide information about the failure cause.

The most detailed form of assertion is calling Assert with two messages:

// Assertion demo
// 
// Purpose: To demonstrate results of different Assert method calls

using System;
using System.Diagnostics;

namespace Assertion
{
    class Application
    {
        [STAThread]
        static void Main(string[] args)
        {
            // Assertion with two additional messages - simple and detailed.
            Trace.Assert(false, "Simple assertion message",
             "This message is just an example. In real applications you
              can provide detailed information here");
        }
    }
}

Now you will see detailed failure information:

Actually, this method is used when checking life-important conditions, such as data correctness, etc. The following code is a usual case when it is used:

public void StoreObject( PersistentObject obj )
{
   Trace.Assert(obj != null, "Cannot store null object");
}

This method verifies that the object that is going to be stored is not null. Often, the failure of such a condition is a good time to raise an exception, but it's not always enough. For instance, if this method stores some program-important data, the assertion is a perfect choice.

Note: At the debugging stage trace is useful, but tracing messages irritate users if they appear in the final release version. Here are some tricks to help you toggle tracing on/off.

If you compile your project manually (i.e. from the command line), tracing is disabled by default. To enable tracing in C#, add the /d:TRACE flag to the compiler command line when you are compiling your code, or you can simply add #define TRACE to the top of your file.

For example, take a look this small application:

using System;
using System.Diagnostics;

namespace TraceShow
{
    /// <summary> 
    /// Summary description for Class1.
    /// </summary>
    class Class1
    {
        /// <summary> 
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
            Trace.Write("Hey, this is a trace message\n", "SIMPLE MESSAGES");
        }
    }
}

If it is compiled in the command line, you won't see anything until you set the /d:TRACE key or add the #define TRACE string to the top of the application.

The situation changes when you compile the application in Visual C#, where tracing is enabled by default. As a result, you'll see all tracing messages. To turn the tracing off, navigate to the project properties (it can be done in the solution explorer or in the View > Property Pages menu. In the Property Pages dialog, find the Conditional Compilation Constants (this option is located in the Build pane of the Configuration Properties folder) and remove TRACE from this option:

 

public static void Fail(string)
public static void Fail(string, string)

The Fail method raises an unconditional assertion. Its behavior is the same as that of the Assert method, but it doesn't require any condition to proceed.

This method can be used if the fail condition cannot be checked with a simple condition. One example of such a situation is exception handling:

try
{
   throw new Exception("Sample exception");
}
catch (Exception ex)
{
   Trace.Fail("Exception caught", ex.Message);
}

public static void Write(object)
public static void Write(string)
public static void Write(object, string)
public static void Write(string, string)

The Trace class can also write trace messages without generating an error. Such messages are posted to trace output devices known as trace listeners. This output is performed by the Write method.

The Write method creates a message which can be represented as an object or a string; In the previous case, the object.ToString() method call is performed. There are three more methods that perform similar actions: WriteLine outputs lines, WriteIf - conditional messages, WriteLineIf outputs a conditional message as a line.

The second parameter of the Write methods specifies a category (i.e. a string) that will be written before messages.

The output is made to all registered listeners. A listener is an object that can output trace messages to some device. Note that the Assert and Fail methods always output error messages to a window or console no matter which listener is currently selected. The class of such an object must inherit the TraceListener class, which has the following important members:

public virtual void Fail(string)
public virtual void Fail(string, string)

public virtual void Flush(string)

public virtual void Write(object)
public abstract void Write(string)
public virtual void Write(object, string)
public virtual void Write(string, string)

public virtual void WriteLine(object)
public abstract void WriteLine(string)
public virtual void WriteLine(object, string)
public virtual void WriteLine(string, string)

The Fail methods don't stop the application here. They only output error messages. The Write and WriteLine methods write a message. The only difference between them is that the latter feeds a line after the output has been performed. Finally, the Flush method flushes buffers. The Flush method works with devices that buffer the trace output (for example, buffered streams). You can also use auto flush - flush listeners after each message. This can be configured in the configuration file:

<configuration>
   <system.diagnostics>
      <trace autoflush="false" />
   </system.diagnostics>
</configuration>

As you can see, if you create your own listener, you should at least implement the Write(string) and WriteLine(string) methods (these methods are the only abstract methods).

Microsoft provides three trace listeners: DefaultTraceListener, EventLogTraceListener and TextWriterTraceListener. The first one has the default behavior (it outputs information to the console if the application is running in the command environment or to the VS Output Window if the application is being debugged there). The second writes trace messages to the specified event log (this works only under Windows systems based on NT Technology - Windows NT, 2K, XP or .NET). Finally the third listener - to the text output stream.

A list of active listeners can be set programmatically or in the configuration file. By default, it includes DefaultTraceListener.

To programmatically change a list of active listeners, you should manage the Listeners collection (with its Add and Remove methods). To manage listeners in the configuration file, you should use the following syntax:

<configuration>
<system.diagnostics>
   <trace autoflush="false" indentsize="4">
      <listeners>
         <add name="myListener" type="System.Diagnostics.TextWriterTraceListener,System"
           initializeData="c:\myListener.log" />
         <remove type="System.Diagnostics.DefaultTraceListener,System"/>
      </listeners>
   </trace>
</system.diagnostics>
</configuration>

The initializeData parameter will be passed to the constructor that takes only a string parameter (e.g. for the EventLogTraceListener listener it sets an event source).

To become familiar with listeners, let's create an application that will use listeners to write trace messages. Its code is very simple:

// Tracing listeners demo
// 
// Purpose: To demonstrate how to use listeners

using System;
using System.Diagnostics;

namespace Assertion
{
    class Application
    {
        [STAThread]
        static void Main(string[] args)
        {
            Trace.WriteLine("Calling WriteLine method", "Tracing listeners demo");
            Trace.Flush();
        }
    }
}

But it's not enough to have code - we also must create a configuration file to set up listeners:

<configuration>
<system.diagnostics>
   <trace autoflush="false" indentsize="4">
      <listeners>
         <remove type="System.Diagnostics.DefaultTraceListener"/>
         <add name="myListener" type="System.Diagnostics.TextWriterTraceListener"
           initializeData="myListener.log" />
         <add name="myListenerEventLog" type="System.Diagnostics.EventLogTraceListener"
          initializeData="Application" />
      </listeners>
   </trace>
</system.diagnostics>
</configuration>

This configuration file removes the default listener and adds two custom ones: The first one outputs messages to a text file, the second, to the event log. For the first listener the initializeData attribute specifies a file to output data to, for the second, the event log to send events to.

Now let's run the application. To check the output of the first listener, we can view the myListener.log file. It must contain the following information:

Tracing listeners demo: Calling WriteLine method

So, we can see that the first listener just writes messages to a text file. Now let's check the second one. To do this, open Microsoft Management Console and navigate to the Application event log (the one that we've specified for the listener in the configuration file) in Event Viewer. You will find the event from our application there. Most possible, it's at the top of the list. If you double-click it, you will see its contents. We had the following information on our machine:


Making the trace output more readable

Sometimes an application is so complex, that the trace itself should be formatted for better understanding. To make the trace log look nicer, you can employ indenting. Take a look at the following sample code:

public void Callee()
{
    Trace.WriteLine("Callee started");

    ... // Some internal logic

    Trace.WriteLine("Initializing buffer");

    ... // Some extra internal logic

    Trace.WriteLine("Exiting callee");
}

public void Caller()
{
    Trace.Write("Caller called");

    ... // Some external logic

    Callee();

    ... // Some extra logic

    Trace.WriteLine("Initializing buffer");
    Trace.Write("Exiting caller");
}

The trace output for this code may look like the following:

Caller called
Callee started
Initializing buffer
Exiting callee
Initializing buffer
Exiting caller

If there were no messages displayed before and after executed methods, the duplicated Initializing buffer message would confuse us. But even this appearance doesn't look clear. To avoid this ambiguity, we use the Trace class that supports indentation. We can modify the existent code taking the advantage of indentation:

public void Callee()
{
    Trace.Indent();
    Trace.WriteLine("Callee started");

    ... // Some internal logic

    Trace.WriteLine("Initializing buffer");

    ... // Some extra internal logic

    Trace.WriteLine("Exiting callee");
    Trace.Unindent();
}

public void Caller()
{
    Trace.Write("Caller called");

    ... // Some external logic

    Callee();

    ... // Some extra logic

    Trace.WriteLine("Initializing buffer");
    Trace.Write("Exiting caller");
}

The output of this modified code will look like the following:

Caller called
   Callee started
   Initializing buffer
   Exiting callee
Initializing buffer
Exiting caller

As you can see, the Callee method output is indented, so we can easily separate messages of different methods. To do this, we have called the Indent() and Unindent() methods.

The Trace configuration can be modified in the configuration file:

<configuration>
   <system.diagnostics>
      <trace indentsize="3" />
   </system.diagnostics>
</configuration>


Debugging

Actually, debugging and tracing have much in common. The methods of the Debug class, where debugging functionality is implemented, have the same names and signatures and almost the same semantics. The only difference is that debugging is disabled in the release configuration (this means that no binary code is produced to call these methods). The debug output can be also set in the configuration file in the following manner:

<configuration>
   <system.diagnostics>
      <debug autoflush="true" indentsize="7" />
   </system.diagnostics>
</configuration>

Note: For debugging, the statements and instructions similar to those employed for tracing are used. The only difference is that the word TRACE is replaced with DEBUG.


Switching debug configurations

The last topic to discuss here is switches. A switch is an object that can have one of several states. Its state can be set in the configuration file or changed programmatically. The switches let you create configurable debugging and tracing code. The best way to get to know switches is to take a look at the sample code:

using System;
using System.Diagnostics;

namespace Switching
{
    class SampleClass
    {
        // Creating a switch. It is initialized by an externally specified value
        static TraceSwitch generalSwitch = new TraceSwitch("CoolSwitch", "Global scope");

        static public void SampleMethod() 
        {
            // The first message is written if the switch state is set to TraceError
            Trace.WriteLineIf(generalSwitch.TraceError, "TraceError message");

            // The second message is written if the switch state is set to TraceVerbose
            Trace.WriteLineIf(generalSwitch.TraceVerbose, "TraceVerbose message");

            // The third message is written if the switch state is set to TraceWarning
            Trace.WriteLineIf(generalSwitch.TraceWarning, "TraceWarning message");

            // The fourth message is written if the switch state is set to TraceInfo
            Trace.WriteLineIf(generalSwitch.TraceInfo, "TraceInfo message");
        }

        public static void Main(string[] args) 
        {
            // Calls the SampleMethod method
            SampleMethod();
        }
    }
}

There are several switch classes: TraceSwitch and BooleanSwitch. In this sample we used TraceSwitch to produce the output according to its state. The switch state is checked by TraceError, TraceInfo, TraceVerbose and TraceWarning properties. These properties test the switch state and return true if the trace level is equal to or greater than the corresponding constant. For example, TraceWarning is true when the level is 2 or greater. The following is a table of level values:

TraceError 1
TraceWarning 2
TraceInfo 3
TraceVerbose 4

The level value can be set in the configuration file in the following manner:

<configuration>
   <system.diagnostics>
      <switches>
         <add name="CoolSwitch" value="3" />
      </switches>
   </system.diagnostics>
</configuration>

But, as we have already said, the state of a switch can be changed from code. We've modified the following code to demonstrate you this:

using System;
using System.Diagnostics;

namespace Switching
{
    class SampleClass
    {
        // Creating a switch. It is initialized by an externally specified value
        static TraceSwitch generalSwitch = new TraceSwitch("CoolSwitch", "Global scope");

        static public void SampleMethod() 
        {
            // The first message is written if the switch state is set to TraceError
            if(generalSwitch.TraceError)
               Console.WriteLine("TraceError message");

            // The second message is written if the switch state is set to TraceVerbose
            if(generalSwitch.TraceVerbose)
                Console.WriteLine("TraceVerbose message");

            // The third message is written if the switch state is set to TraceWarning
            if(generalSwitch.TraceWarning)
                Console.WriteLine("TraceWarning message");

            // The fourth message is written if the switch state is set to TraceInfo
            if(generalSwitch.TraceInfo)
                Console.WriteLine("TraceInfo message");
        }
 
        public static void Main(string[] args) 
        {
            Console.WriteLine("Before manual level set\n");

            // Call the SampleMethod method with configuration parameters
            SampleMethod();

            Console.WriteLine("\nAfter manual level set\n");

            // Set trace switch level
            generalSwitch.Level = TraceLevel.Warning;

            // Call the SampleMethod method with manual parameters
            SampleMethod();
        }
    }
}

Now if we run the application with the same configuration, we'll obtain the following output:

Before manual level set

TraceError message
TraceWarning message
TraceInfo message

After manual level set

TraceError message
TraceWarning message

This shows that the trace switch level has been changed.


Timing the Performance

In this section we'll tell you how much time may be spent on debugging. Actually, debugging has nothing to do with business logic, but execution of debugging code takes some time. We'll measure the time spent on outputting debug messages in applications. For instance, this measurement is important when you are testing a time-critical application. Take a look at the following code:

using System;
using System.Diagnostics;

namespace DebugDemo
{
    class PrimeNumberDetector
    {
        public static bool IsPrime( int n )
        {
            int upperbound = (int)Math.Sqrt(n);

            for (int i = 2; i <= upperbound; i++)
            {
                Debug.WriteLine("Processing number " + n + ", testing with " + i);
                if ((n % i) == 0)
                {
                    Debug.WriteLine("FAILED");
                    return false;
                }
            }
            return true;
        }
    }

    class Application
    {
        [STAThread]
        static void Main(string[] args)
        {
            for (int i = 2; i < 10000; i++)
                if (PrimeNumberDetector.IsPrime(i))
                    Console.WriteLine("{0} is a prime number", i);
        }
    }
}

This program tests integers from 2 to 10000 for primality and outputs prime numbers. For debugging purposes we output each number being tested, no matter whether it's a prime number or not. If a number is not prime, we output FAILED.

Now we'll measure the time spent on the script run with enabled and disabled debugging.

  1st test 2nd test 3rd test
With debugging
(hh:mm:ss.ff)
 00:00:07.9714624   00:00:07.9414192   00:00:07.9714624 
Without debugging
(hh:mm:ss.ff)
 00:00:05.1273728   00:00:05.5179344   00:00:05.1273728 

As you can see, debugging is quite expensive - in our case it took 64% of the execution time.


Conclusion

In this article we have described general approaches to debugging and tracing .NET applications. Of course there are some other issues, for instance, conditional compilation, that haven't been covered here. To learn more about them, see MSDN. We hope this article will help you master debugging and tracing techniques of .NET applications.

Copyright © 1999- 2008, AutomatedQA, Corp. All Rights Reserved.
Home | Legal | About | Contact | Site Map | Print