Custom ASP.NET TraceListener for Debugging
By Peter A. Bromberg, Ph.D.
Printer - Friendly Version
Peter Bromberg

Asserts - key to the Debugging Palace

Debug.Assert() is not well known in the .NET programming space. However, it is easy to understand and can be one of the most useful tools in your arsenal when writing and debugging .NET code.

To simplify, the way Assert works is that when your code runs, you expect some variable or expression at a particular point in your code to always be true. If the condition is ever not true, you want to know about it right away because you may have a bug. Assert statements give you a productive way to zero in on the exact point of failure, and ONLY at the exact point in time that the failure has actually occurred, such as the sixteenth time through a "foreach" loop.



The best way to catch these errors is to have a way to show an "up-front, in your face" message that unambiguously shows you that the suspected condition has occurred. Even better, if you are testing your code in Debug mode, you probably want to have the code immediately break right into the debugger at or near the line of code where the error occurred. And if your code is NOT running in Debug mode, you want these conditions to have no effect, and if possible, not even to have any compiled code to bloat your assembly at all, without you having to unwire or comment - out blocks of  "test code" just to make a RELEASE build.

Conditional compilation constants with #DEFINE statements and #IF --- etc. statements, long used by C++ programmers, are also available in managed code. However, there is an even easier and much more elegant way to wire up code with such conditional statements or methods- the Conditional attribute:

[Conditional ( "DEBUG" )]
private void DebugOnly()
{
lblMessage.Text+="<br/><font color=red>This conditional method is called on DEBUG only!</font>";
}

Yes, the above method will ONLY be available when you run your ASP.NET project in DEBUG mode. It is ignored by the compiler in RELEASE mode.  And, so are any in-code calls to the method - they are simply ignored by the compiler on a RELEASE build!

Of course, you are free to declare your own conditional attributes. If the above attribute was {Conditional("HAMBURGER")], and I had a #DEFINE HAMBURGER statement at the top of my class file, it would work fine just the same. The built-in defaults are, of course, DEBUG and RELEASE.

The Assert method is preferable to popping up a MessageBox or other efforts because it brings up a dialog that offers to run the debugger, and also provides a stack trace right out of the box with no effort on your part:

Using Asserts with ASP.NET

But, there is one big problem. . . Have you ever tried to do a Debug.Assert in ASP.NET? Doesn't work, does it? No, because ASP.NET doesn't support Winforms - based dialog boxes. The reason for this should be obvious - 99% of the time, there is nobody logged on to the webserver machine to see the dialog and dismiss it. In addition, you probably want this action to occur ONLY when it's on your local machine that you are developing on, and NEVER when the application is called from a remote machine at all, even in DEBUG mode.

As luck would have it, the developers of the ASP.NET runtime saw fit to make many of the Base classes and methods overridable and customizable. The Debug and the Trace classes can both be customized to provide custom functionality. These classes both delegate to System.Diagnostics.TraceListener. So by simply creating our own AssertHandler class, deriving from TraceListener and overriding the default methods, we can indeed create an elegant and useful way to get exactly what we want in ASP.NET - an Assert Dialog that pops up only when an Assert statement is fired, complete with the Stack Trace and the optional button to break into the debugger. On top of that, we can easily wire it up so that it only happens when we are debugging the project on our LOCAL MACHINE.

The Framework comes with some predefined TraceListeners that you can add to the Listeners array for additional outputs. The first is the EventLogTraceListener class, which will send output to the specified event log. The second is TextWriterTraceListener, which directs output to a TextWriter or Stream, such as the Console.Out function of FileStream. For example, if you wanted to add a TextWriterTraceListener to the chain:

Debug.Listeners.Add(new TextWriterTraceListener ( "Trace.Log" ) );

What I've chosen to do here is implement a small class, "AssertHandler.Assert", that overrides the default methods in the base class to accomplish what we want to do. In this manner, we only need to add one TraceListener, and it will handle all the cases:

using System;
using System.Web;
using System.Diagnostics;
using System.Windows.Forms;

namespace AssertHandler
{
public class Assert : DefaultTraceListener
{
public override void Fail(string message)
{
// plain deal with no override specialties
Fail(message,null);
}


public override void Write(string message1)
{
// same here

}

public override void Fail(string message1, string message2)
{
HttpRequest req=HttpContext.Current.Request;
if(req==null || req.UserHostName==req.ServerVariables.Get("LOCAL_ADDR"))
{
StackTrace t = new StackTrace();
string s = t.ToString();
// if they put "PAGE" at the beginning of the second message,
// write trace directly to page instead of dialog with debugger trigger

if(message2.Substring(0,4)=="PAGE")
{
HttpContext.Current.Trace.IsEnabled=true;
HttpContext.Current.Trace.Write(message1 + "\r\n" +message2 + "\r\n" + s);
return;
}

if(message2.Substring(0,3)=="LOG")
{
// if they put "LOG" at the beginning of the second message,
// write trace to Event Log entry instead of dialog with debugger trigger , showing the Page name

System.Diagnostics.EventLog.WriteEntry(
    HttpContext.Current.Request.ServerVariables["SCRIPT_NAME"].ToString(),
     message1 +"\r\n" +message2 + "\r\n" +s);
return;
}

string strDisplay =message1 + "\n" + message2 + "\n" + "Launch Debugger?\n\n" + t.ToString();
DialogResult r= MessageBox.Show(strDisplay, "Assertion failed.",MessageBoxButtons.YesNo,MessageBoxIcon.Error
,MessageBoxDefaultButton.Button1,MessageBoxOptions.DefaultDesktopOnly);

if(r==DialogResult.Yes)
Debugger.Break();
}
}
public Assert()
{
}
}
}

You can add this to your Global class as follows:

protected void Application_Start(Object sender, EventArgs e)
{
Debug.Listeners.Clear();
Debug.Listeners.Add(new AssertHandler.Assert());
}

-- and you are all wired up to go!

Now you have three options:

First, you can do a Debug dialog:

private void Button1_Click(object sender, System.EventArgs e)
{
Session["ctr"]=(int)Session["ctr"] +1;
Debug.Assert((int)Session["ctr"] % 3!=0,"Assert Fired!","Counter=" + Session["ctr"].ToString());
}

Second, you could do a Trace to the page:

private void Button2_Click(object sender, System.EventArgs e)
{
Session["ctr"]=(int)Session["ctr"] +1;
Debug.Assert((int)Session["ctr"] %3!=0,"Assert Fired!","PAGE:Counter=" + Session["ctr"].ToString());
}

Finally, you could choose only to write the information to the event log:

private void Button3_Click(object sender, System.EventArgs e)
{
Session["ctr"]=(int)Session["ctr"] +1;
Debug.Assert((int)Session["ctr"] %3!=0,"Assert Fired!","LOG:Counter=" + Session["ctr"].ToString());
}

The downloadable solution illustrates possible usage for all three methods, and I am sure you can think of more! Just right-click on the folder you unzip it into, choose Sharing and Web Sharing, share the folder to make an IIS virtual directory, and you are ready to load and run the solution out of the box.

 

Download the code that accompanies this article


 


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.