Cutting Edge

MyTracer Monitors and Traces ASP.NET Apps

Dino Esposito

Code download available at:CuttingEdge0304.exe(187 KB)

Contents

Trace-enabled ASP.NET Pages
Emitting Trace Messages
The Trace Viewer
The MyTracer Tool
Enabling MyTracer
The Helper Web Service
The Windows-based Application
Putting It All Together

The Microsoft® .NET Framework comes with a rich set of programming tools for debugging and tracing applications. I'm not talking about integrated debuggers; I'm referring to software components that you use in the development cycle. Debuggers are a different animal because they are interactive tools that normally integrate with a full-fledged IDE such as Visual Studio® .NET.

The Systems.Diagnostics namespace defines two classes, named Trace and Debug, whose methods can be used to output messages containing information about code execution. This feature is extremely useful for tracking data inconsistencies, monitoring execution flow and assert conditions, and even gathering profiling data. The Trace and Debug classes are nearly identical and work on top of more specialized modules called listeners.

The tools you use for tracing Windows® and ASP.NET applications have rather different characteristics. In particular, the tracing mechanism supported by Windows Forms applications has an extensibility model based on custom listeners; the trace mechanism for ASP.NET applications does not. Instead, the ASP.NET subsystem provides a custom tracing model. When a page executes in trace mode, a variety of tables are appended to the output and they display information about performance, requests, and state. Regardless of whether you write messages to the page's trace log, those tables are always appended at the bottom of the output sent to the browser.

Visual Studio .NET provides a powerful, integrated debugger that supports cutting-edge features such as breakpoints, quick watch, and various ways of stepping through the code. The debugger is the ideal tool to catch errors and step into small but critical portions of the code. So if you use Visual Studio .NET to build your project, you can't ask for more. But what if you don't use Visual Studio .NET? What if you use an editor that doesn't provide debugging support? In such situations, the importance of programming tools for tracing the behavior of an application grows considerably.

In this column, I'll first examine the tracing subsystem available to all ASP.NET applications and then write a Windows Forms application that executes a given page and collects runtime information such as the page's view state, the request context, and the contents of global objects like Cache, Application, and Session. The application, which I've called MyTracer, can be integrated with Visual Studio .NET and constitutes an alternative to the default browser, at least during the testing phase. I should point out that in certain places I'll be using undocumented classes. It's unlikely to affect the code, but those classes are subject to future changes.

Trace-enabled ASP.NET Pages

Although a trace configuration section can be added to the web.config file to configure tracing at the application level, tracing is a feature that you often control on a per-page basis. For large projects, you can toggle the tracing attribute on and off using the following code in the application's web.config file:

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

The enabled attribute authorizes tracing on the application, while the pageOutput attribute permits output to appear in the page. If pageOutput is set to false (which is the default setting), then the tracing output is automatically routed to the ASP.NET tracer tool, trace.axd. At the end of the project, you simply set both of the trace element's attributes to false. This way, you eliminate the risk that you'll inadvertently leave tracing enabled.

To enable tracing only on a particular page, you use the trace attribute in the @Page directive. The default value for the attribute is false. When you set it to true, both system and custom tracing information appear at the bottom of the page, as shown in Figure 1.

Figure 1 System and Custom Tracing

Figure 1** System and Custom Tracing **

You should note that the trace information is part of the actual page output and, as such, displays through any type of browser that accesses the page. As you can guess from the figure, some tables of information show up along with the trace information generated by the page. Additional tables display request details, the control tree, and some useful collections such as cookies, headers, form values, and server variables. If the session and the application state are not empty, the contents of the Session and Application intrinsic objects are also included in the view. The contents of the view state and the Cache object are not flushed to the trace log. These are the key enhancements that my tool will provide.

The @Page directive also supplies the TraceMode attribute to let you choose the order in which the information should be displayed. Feasible values are the strings SortByCategory and SortByTime. By default, the trace messages appear in the order they are emitted. If you set the TraceMode attribute to the SortByCategory value, then the rows appearing in the Trace Information section are sorted by category name. The category to which each row belongs is determined by the method used to emit the message.

Emitting Trace Messages

An ASP.NET page populates its trace log using methods exposed by the TraceContext class. An instance of this class is created when the HTTP request is set up for execution. The tracer object is then exposed through the Trace property of the HttpContext class and is also mirrored by the Trace property on the Page class.

The TraceContext class has quite a simple interface, featuring a couple of properties and just as many methods. The properties are IsEnabled and TraceMode. IsEnabled is a read-only Boolean property that indicates whether tracing is enabled. The value that this property returns is affected by the trace attribute on the @Page directive as well as the enabled attribute in the trace section of the web.config file. The TraceMode property gets and sets the order in which the traced rows will be displayed on the page. The property is of type TraceMode—an enumeration that includes special values such as SortByCategory and SortByTime.

To emit messages, you can use either the Write or Warn methods. Both methods have three overloads which behave in the same way. Write and Warn are nearly identical methods. The only visible difference is that Warn always outputs messages in red.

The Write method has three overloads:

Public Sub Write(msg As String) Public Sub Write(cat As String, msg As String) Public Sub Write(cat As String, msg As String, e As Exception);

The simplest overload just emits the specified text in the message column. In the second overload, the first string argument represents the name of the category you want to use for the message, which is the second argument. The category name can be used to sort trace information and is any name that makes sense to the application to better explain the message. Finally, the third overload adds an extra Exception object in case the message is tracing an error. In this case, the text in the message column is created by the concatenation of the text you specify as an argument and the message about the exception.

Although the text output by both Write and Warn is rendered as HTML pages, no special styling or formatting is applied. The strings are written as plain text, so if you attempt to use any special formatting such as bold tags, all you'll get is a trace message with visible <b> and </b> substrings attached.

The ASP.NET Trace object is accessible without a fully qualified name from the source code of the .aspx page or from the codebehind class. Custom controls embedded in the page and their codebehind classes can also access the tracing subsystem directly. Other classes can't, however. Suppose that your codebehind class delegates an external class to accomplish some tasks. How could this worker class perform tracing in the ASP.NET page? In the context of the worker class, the page Trace object is not available, at least not in its unqualified, direct form. External classes that want to emit text in the trace log of the current HTTP request must call the tracer using the following expression:

System.Web.HttpContext.Current.Trace.Write(cat, msg)

Note that the ASP.NET tracing system does not support its own set of listeners, nor does it support those registered through the diagnostic trace section. In addition, there's no known way to modify the output of the trace to show only a few of the standard tables of data. If you want to enhance or modify it, you need a completely new approach.

The Trace Viewer

ASP.NET also supports application-level tracing through the trace viewer tool. Once tracing has been enabled for the application, each page request routes all the page-specific trace information to the viewer. You can look at the trace viewer by requesting trace.axd from the root application directory (see Figure 2). As I mentioned earlier, you enable the viewer by placing a trace section in the application's web.config file—that is, the configuration file deployed in the root folder:

<configuration> <system.web> <trace enabled="true" /> </system.web> </configuration>

Since the pageOutput attribute is false by default, only the viewer receives trace information. However, each page can individually override this setting by using the trace attribute on the @Page directive. The trace viewer caches no more than the number of requests specified by the requestLimit attribute (10 by default).

Figure 2 Viewing the Trace Viewer

Figure 2** Viewing the Trace Viewer **

In brief, the ASP.NET trace viewer acts as a centralized console and gathers all the trace information generated by the pages in a particular application. Each request, up to the maximum number fixed by requestLimit, is identified by a row in the viewer's interface and can be consulted until the viewer's cache is cleared (see Figure 2).

You activate the viewer by requesting the trace.axd URL on the root of the application. An AXD is a special resource type that is resolved through the aspnet_isapi.dll ISAPI extension, as Figure 3 shows. At first, the page in Figure 2 is displayed. After that, the viewer automatically tracks all requests and caches the full trace for each. When the request limit is reached, no other request will be cached until the log is manually cleared. You can click on each View Details link to see the trace for that particular request.

Figure 3 Requesting a Trace

Figure 3** Requesting a Trace **

The built-in mechanism for tracing is perfect for ASP.NET in the sense that it provides a good deal of runtime information and allows you to assert and verify dynamic values of internal structures. On the other hand, it doesn't show all the information you might want to track, and the text-based user interface is boring. The trace information is appended to the page after the last lifecycle event of the page fires. As you know, an ASP.NET page works by handling the events it receives from the HttpRuntime environment. These events describe the page lifecycle and move from Page_Init to Page_Unload through a handful of situations including Page_Load and Page_PreRender. When the final event is being generated, the HTML code for the browser has not been prepared yet. When the page renders, the HTML is temporarily cached in the output stream and left at the disposal of registered modules for further processing. As a result, the code in the page can't even access the HTML information attached as trace output. This closed structure, along with my dislike for the bland text-based user interface, sent me on a quest for an alternative scheme.

Before discussing MyTracer, I should make a point about tracing in general. While you should pay careful attention not to deploy pages with the trace attribute turned on, tracing calls can be left in the code for future use. Both the Write and Warn methods, in fact, return immediately if tracing is not enabled. This may have an impact on performance, but it will be negligible, especially since the tracing mechanism can be turned on declaratively without touching the code. By simply changing the trace statement's enabled attribute in the web.config file, you enable tracing through the viewer. In this way, you can monitor the application after some user feedback, for example, without stopping it. For the viewer to work, though, trace information must be conditionally emitted.

The MyTracer Tool

There are other ways to write an alternative tracer tool in ASP.NET 1.x. You could write it as an HTTP module and use both the HttpApplication events and the ASP.NET object model to collect the runtime information to display. In addition, you could make it expose methods and events to interact with the page. In this column, I'll propose a different approach that exploits the rich user interface of Windows Forms applications and integrates with Visual Studio .NET. At the end of the day, MyTracer is a Windows Forms program that embeds the WebBrowser ActiveX® control to display Web pages and makes use of a Web Service as the bridge between ASP.NET and Windows. Figure 4 shows the tool in action on a sample page.

Figure 4 MyTracer Tool

Figure 4** MyTracer Tool **

The tool navigates to the specified page and displays it. If the page has been configured to work with MyTracer, some page-specific information is exported and made available to a helper Web Service located in the same virtual folder as the application. The Windows Forms application handles the DocumentComplete event and, when ready, connects to the Web Service to download the ASP.NET runtime information. After that, it simply populates the various tabs with page-specific data such as the contents of Session, Application, Cache, the view state, the input form, server variables, and even the list of the server controls in the page.

MyTracer contains three components—the Windows Forms program shown in Figure 4, the aforementioned Web Service, and a Web user control that the traced page must contain. The user control—mydebugtool.ascx—hooks up the key events in the page lifecycle and stores information into a DataSet object. When the page unloads, the DataSet is copied into a database in a way that closely resembles the SQL Server™ implementation of the ASP.NET session state. The Web Service retrieves any data from the database and serializes it down to the Windows Forms application. The data always travels as a DataSet with one child table per tab in the user interface. The database and the Web Service each have extremely simple structures. In particular, the DataSet is persisted as an XML DiffGram so that it is treated and moved as a string. Figure 5 depicts the overall architecture.

Figure 5 MyTracer Architecture

Figure 5** MyTracer Architecture **

Enabling MyTracer

To be traced, a page must contain the MyDebugTool user control. The control has a simple user interface that tells the user that the tool is in action. (See the link at the top of this article.) The control hooks up the Page_Unload and Page_PreRender events and caches application- and request-specific information such as Application, Cache, and Session. The data container is a DataSet object that is instantiated within the control's constructor and is persisted to a SQL Server database as the last action when the control is signaled that the page is unloading. Given an existing ASP.NET page, to enable MyTracer you must register the MyDebugTool control and add it to the control collection. A user control is registered through the @Register directive:

<%@Register TagPrefix="msdn" TagName="debug" Src="MyDebugTool.ascx"%>

Note that TagPrefix and TagName can take any value and that the user control must reside in the same Web domain as the application. You can place an instance of the control anywhere in the page, even at the very top. Given the registration I just showed, you use the following code:

<msdn:debug runat="server" ID="mytracer" UserKey="dino" />

The ID is not strictly necessary but, as you'll see later, you must assign one if you want to access view state information. The UserKey attribute—a public property on the user control class—is used to identify the database row where all the information will be parked. It has to be a unique name, but there are no other requirements. When you're done with these configuration tasks, point any browser to the page. The page displays a blue strip with some text stating that the page is now under the control of the MyTracer tool. As soon as the page is sent to the browser, the database is updated. The connection string for the database can be set at will using the ConnString property. It defaults to a SQL Server database called MyTracer with a table named InternalCache. The table features two fields—UserKey and Data. UserKey is a string; Data is a Text field that contains the DiffGram representation of the DataSet that contains all the collected information. As of this writing, the code assumes that a record with the specified userkey already exists in the database. In other words, only UPDATE statements are used and INSERT's are never performed. As long as the following query can be successfully understood and executed, you can use any database on any server with any login.

SELECT data FROM internalcache WHERE userkey=@TheUser

The user control creates a number of tables in a temporary DataSet. All tables have two string columns named Key and Value. The control creates tables to hold the contents of the following collections: Request.Headers, Request.Form, Request.Cookies, Request.ServerVariables, and Application, Cache, Session, plus the page's view state and controls. The code in Figure 6 shows how information about the Cache and the page controls is retrieved. In particular, the Cache object contains both user and system information. As the tracer shows, a few slots in the collection are managed by the server-side HTTP runtime and point to internal classes used to contain runtime information. For example, if the session state is configured to be local to the ASP.NET process (which is the default), then the collection of session items is stored as a single object in Cache. The internal session collection is called SessionStateItem, as Figure 7 illustrates.

Figure 7 System Items Stored in Cache

Figure 6 Retrieving Cache and Page Control Information

// ***************************************************************** // Load information from cache private void LoadFromCache(DataSet info) { DataTable dtCache = CreateKeyValueDataTable("Cache"); foreach(DictionaryEntry elem in Page.Cache) { if (ShowAll) { AddKeyValueItemToTable(dtCache, elem.Key.ToString(), DisplayFormat(elem.Value)); } else { string s = elem.Key.ToString(); if (!s.StartsWith("ISAPIWorkerRequest") && !s.StartsWith("System")) AddKeyValueItemToTable(dtCache, elem.Key.ToString(), DisplayFormat(elem.Value)); } } dtCache.AcceptChanges(); info.Tables.Add(dtCache); return; } // ***************************************************************** // ***************************************************************** // Load information from page's controls private void LoadFromPageControls(DataSet info) { HtmlForm theForm = null; DataTable dtControls = CreateKeyValueDataTable("Controls"); for (int i=0; i<Page.Controls.Count; i++) { if (Page.Controls[i] is HtmlForm) theForm = (HtmlForm) Page.Controls[i]; AddKeyValueItemToTable(dtControls, Page.Controls[i].ToString(), Page.Controls[i].ClientID); } dtControls.AcceptChanges(); info.Tables.Add(dtControls); if (theForm == null) return; DataTable dtFormControls = CreateKeyValueDataTable("FormControls"); for (int i=0; i<theForm.Controls.Count; i++) AddKeyValueItemToTable(dtFormControls, theForm.Controls[i].ToString(), theForm.Controls[i].ClientID); dtFormControls.AcceptChanges(); info.Tables.Add(dtFormControls); return; } // *****************************************************************

Looking at Figure 7, you can see that all the system items stored in the cache have a name that starts with either "System" or "ISAPIWorkerRequest." If you want to see the items that any sessions in the application placed in the cache, set the ShowAll property of the MyDebugTool control to false. In Figure 7, some values in the Value column are wrapped by curly brackets, meaning that the item contains an object and the displayed string is the name of the class. The following code examines the object's type and returns the class name or the actual value, as appropriate:

string DisplayFormat(object o) { return (o is string || o.GetType().IsPrimitive ?Convert.ToString(o) :"{ " + o.ToString() + " }"); }

The IsPrimitive property on the Type class returns true if the type is a numeric type, char, or Boolean. Note that in this context strings are not considered primitive types; rather, they're treated as a reference type and as instances of the System.String class.

The code used to extract all the information stored in the Cache is similar to the code used to extract the contents of Application, Session, or any Request collection.

To list the controls in the page, you loop through the Page.Controls collection. Note, though, that the collection covers only the first level of controls—that is, only the controls that are direct children of the body element in the overall HTML model of the page. In practice, if you were to loop through the Page.Controls collection, you get a very small number of controls—mostly literals and the ASP.NET unique form control. By design, the server controls that make the page work are contained within a server-side form. To access all of them, you must perform a second loop based on the form's Controls collection. To obtain the full control tree, as in the default ASP.NET trace mode, you must recursively loop through all the Controls collections of all listed controls. The MyTracer tool displays the ID of each control. Note that, by design, all server controls must have a client ID. This ID has to be unique and is normally assigned based on the value of the server-side ID property. If the server control doesn't have an ID, the ASP.NET runtime generates one that is guaranteed to be unique within the page. To access the client-side ID, use the ClientID property.

The Helper Web Service

Architecturally speaking, a Web Service is rather interesting because it can be used to create a bridge between ASP.NET and desktop applications and even between different ASP.NET applications. In this case, the Web Service is the element that glues together the Windows Forms program and the monitored ASP.NET page. It provides a common interface to allow the application to obtain ASP.NET-specific information. I used a Web Service here because it was easy to work with. Other possibilities exist, too. For example, you could use an external Windows-based service that operates through remoting, getting data from the user control and handing it over to the Windows Forms application. Similarly, the choice to use a database is a personal one. You could use global memory within the service instead. Overall, the StateServer and SqlServer options that you have for storing the session state of an ASP.NET app can be replicated for MyTracer.

The programming interface of the Web Service is extremely simple (see Figure 8). The GetInfo method executes a query against the database and gets back a string that represents the XML DiffGram representation of the original DataSet. GetInfo deserializes the string and rebuilds a valid DataSet object using the DataSet's ReadXml method. Next, the newly created and populated DataSet object is returned to the caller. Note that if the UserKey is left unchecked, the Web Service is susceptible to a SQL injection exploit. Ideally you would want to either scrub the UserKey field or use a parameterized query.

Figure 8 Web Service Programming Interface

<%@ WebService language="C#" class="MyDebugTool" %> using System; using System.Web.Services; using System.Data; using System.IO; using System.Data.SqlClient; [WebService(Namespace="MsdnMag.CuttingEdge")] public class MyDebugTool { [WebMethod] public DataSet GetInfo(string connString, string userKey) { SqlDataAdapter adapter = new SqlDataAdapter( "SELECT data FROM internalcache WHERE userkey='" + userKey + "'", connString); DataTable tmp = new DataTable(); adapter.Fill(tmp); DataSet ds = new DataSet(); StringReader reader = new StringReader(tmp.Rows[0]["data"].ToString()); ds.ReadXml(reader); ds.AcceptChanges(); return ds; } }

The Windows-based Application

The MyTracer application employs a tabstrip control and the WebBrowser ActiveX control to display the page, thus triggering the user control. When the page download is complete, the application calls the Web Service and the GetInfo method to bind data to the other tab pages. The following code shows how to write handlers for a couple of WebBrowser's events—StatusTextChange and DocumentComplete:

// Hook up the StatusTextChange event DWebBrowserEvents2_StatusTextChangeEventHandler e1; e1 = new DWebBrowserEvents2_StatusTextChangeEventHandler(StatusTextChange); webBrowser.StatusTextChange += e1; // Hook up the DocumentComplete event DWebBrowserEvents2_DocumentCompleteEventHandler e2; e2_= new DWebBrowserEvents2_DocumentCompleteEventHandler(DocumentComplete); webBrowser.DocumentComplete += e2;

The information available for the event is packed in a custom event data class whose name matches that of the event handler. For example, the name of the class is DWebBrowserEvents2_DocumentCompleteEvent for the DocumentComplete event.

All the page tabs have a common structure, containing one or more DataGrid controls that are bound to one of the tables in the DataSet retrieved via the Web Service.

The tracer application also retrieves the page's view state (see Figure 9). This feature goes beyond the capabilities of the ASP.NET default tracer, which is limited to displaying the size (in bytes) of the view state for each control on the page. MyTracer only provides information about the view state elements stored within the page's view state. It doesn't cover the state elements created by individual controls. As I discussed in the February 2003 installment, the ASP.NET view state is created by aggregating the view state of each constituent control with that of the host page. The view state of a page or a control is a StateBag object exposed by the ViewState property. The rub, though, is that ViewState is a protected property and this level of protection makes the information inaccessible from within external components.

Figure 9 View State

Figure 9** View State **

So how can the user control retrieve the view state of the host page? One possibility that I haven't yet fully explored and investigated is reflection. In theory, the Reflection API in the common language runtime allows you to programmatically access internal and non-public elements of a class. However, it has yet to be proven that this can be done from within the security context of an ASP.NET application. (By the way, if you experiment and obtain some interesting results, please let me know!)

MyTracer uses a more direct but less powerful way to read the page's view state. The user control simply makes it possible for the page (and subsequently for the page's author) to deliberately expose its view state to the tool. The MyDebugTool control features a BindViewState method that the monitored page can call at any time and pass its view state object, as shown here:

void Page_Load(object sender, EventArgs e) { ••• // Bind the viewstate to display mytracer.BindViewState(ViewState); }

In this way, the page can also pass on any view state object it may hold—for example, the view state that a custom control may decide to expose for debugging purposes at development time. A simple trick you can use to look into a control's view state is to create a wrapper class for the control that exposes only the ViewState property. For example:

public class MyDataGrid : DataGrid { public StateBag ExternalViewState { get {return ViewState;} } }

Next, use the new control in the page and bind the exposed copy of the view state to the MyTracer application. The trick doesn't work if the control class is sealed and can't be derived any further. So if you're developing a commercial control and don't want users to spy on your control's state, sealing the class for inheritance would do the trick. However, this choice has its pros and cons. For example, I wouldn't buy a third-party control that I couldn't inherit from. Also, note that I'm simply talking about spying the view state, not modifying it. Modifying the view state is virtually impossible, as I discussed in my February 2003 column.

Putting It All Together

The MyTracer application is useful when you're developing and testing a particular ASP.NET page. The tool runs the page, captures any runtime information, and displays it to you in a friendly way. It also provides more information than you can view while tracing the page through traditional methods. This version of the code doesn't support the output of custom messages, nor does it capture the output you sent using the Trace object. However, adding a message container is as easy as adding a new Write-like method to the user control and collect all passed strings in another table.

MyTracer's purpose is to serve as a helper tool that can replace the browser while testing an ASP.NET application, even within Visual Studio .NET. In the Configuration Properties of your Visual Studio ASP.NET project, check the Start external program option, type in the path to MyTracer, and set the page URL as the command-line argument for the tool (see Figure 10).

Figure 10 Setting Up MyTracer in Visual Studio .NET

Figure 10** Setting Up MyTracer in Visual Studio .NET **

You should note that while using MyTracer, the ASP.NET debugging feature doesn't work very well. If you need to step into the code, nothing works better than the debugger, where the Quick Watch feature allows you track just about any value in the application. If you don't need to debug, then MyTracer is certainly more helpful than the browser. To enable the debugger again, click on the Start project option in the same dialog (see Figure 10).

In the code download for this column, you'll find the source code for MyTracer as well as a sample ASP.NET project to experiment with. I can't wait to hear your comments!

Send your questions and comments for Dino to cutting@microsoft.com.

Dino Espositois an instructor and consultant based in Rome, Italy. Author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for .NET, both from Microsoft Press, he spends most of his time teaching classes on ASP.NET and speaking at conferences. Dino is currently writing Programming ASP.NET for Microsoft Press. Get in touch with Dino at dinoe@wintellect.com.