| Handling Assertions in ASP .NET Web Apps |
|John Robbins |
| Browse the code for this article at Code Center: ASP Assertion|
| ecently I was moving along with my ASP .NET Web site de-sign and was quite pleased to see just how much easier ASP .NET made the whole process. Being a good boy, I went through the design process and was about to start development when I noticed a small problem. A prototype I developed to test a theory would hang when I used the standard Debug.Assert. |
If you hearken back to the February 2001 Bugslayer column, you'll remember that I talked about the TraceListeners, which do the actual output for the System.Diagnostic namespace's Debug and Trace objects. In that column, which I wrote back in the dark ages of the 2000 PDC release of the Microsoft® .NET Framework, I mentioned that the DefaultTraceListener would pop up a message box on assertions only when running with an interactive user logged in. As I was staring at my blocked ASP .NET thread, now running under Beta 2, I had a sinking feeling that the Debug.Assert had popped up a message box. A little exploration with a native debugger and sure enough, a message box off in some la-la land WindowStation was waiting for me to press the OK button, since ASP .NET runs as a Win32® service.
At first, I didn't quite believe that using Debug.Assert would cause such a problem. As all loyal Bugslayer readers know, assertions are next to godliness in the scheme of life. Without assertions, you might as well not program because you have no way of knowing what happened in your code without them. Of course, I immediately stopped working on my project and set out to fix the situation. After a couple of false starts on my part, it turned out to be moderately easy to solve. In this installment of the Bugslayer, I'll give you a very clean way to handle assertions in your ASP .NET Web applications.
When I set out to solve the ASP .NET assertion problem, I jotted down a list of requirements that I wanted with the fixed assertion. That way, I had a solid goal in mind before I started hacking away. The first thing I wanted was the ability to put the output on the page because that's where it would do the most good. Additionally, I wanted the output to go through the excellent ASP .NET tracing mechanism turned on using web.config or the page's Trace directive. For more information on ASP .NET tracing, see Rob Howard's Nothin' but ASP .NET column or the Visual Studio® .NET release of MSDN®. As I was thinking about the page output, I felt it would also be a good idea to offer other places for output, such as the event log or traditional OutputDebugString calls. I could easily envision large Web farm developers wanting to set a farm-wide output standard. Finally, I wanted to be able to change the output on the fly.
The second major requirement I wanted in my assertion code was the ability to break into the debugger or launch a debugger when the server processing encountered the error. While it was great to see assertion output after the fact, assertions work best when you get a debugger attached so you can look at the problem as it's happening.
As I was thinking of the output, a tough realization dawned on me. Since the existing DefaultTraceListener will pop a message box no matter when a call to Debug.Assert fails, all .NET developers are in a world of hurt. Say you are developing a Web site for actuaries to do insurance calculations. Since those actuarial calculations are proprietary, you will probably spend quite a bit of money to get a calculation library. If the company that sold you the library is smart, they will have ported their calculation engine over to .NET so you can have managed code throughout.
That's reasonable, but what if the calculation engine developer is using Debug.Assert throughout the debug build of the library? Worse yet, what if they only tested with Windows® Forms applications? Since you want to take that library and use it in an ASP .NET application, you are in trouble. Thinking through this, I realized that my last major requirement was that I needed to ensure that I grabbed all possible calls to Debug.Assert. No matter if you or some third-party library written without ASP .NET in mind are making them, this will ensure that your app will never hang. Now that you have the requirements, I'll discuss how to use my code, which will make the implementation details easier to understand.
Using the Bugslayer Assertion
All of this month's code (which can be found at the link at the top of this article) is in a single assembly, appropriately named Bugslayer, that I will continue to update on the Wintellect Web site (http://www.wintellect.com) as I did with the Win32 BugslayerUtil.DLL over the last few years. There are two parts to using the Bugslayer assertion. The easiest is simply deriving your application class, generally found in GLOBAL.ASAX, from Bugslayer.AssertHttpApplication, instead of System.Web.HttpApplication. All the AssertHttpApplication class does is clear out any existing TraceListeners from the System.Diagnostic.Debug and System.Diagnotic.Trace classes and adds the single application-wide ASPTraceListener class to both. It also adds the ASPTraceListener class to the application-wide bag state so that you can get at it whenever you need to. Of course, you'll need to reference BUGSLAYER.DLL in your project as well in order to get everything built.
With just the single derivation, all Debug.Assert output and Trace.WriteXxx output will show up in the standard ASP .NET tracing. Figure 1 shows an example. As you can see, you'll get full source and line information, so it's quite useful.
Figure 1 Assertion Output in the ASP .NET Trace Log
The ASPTraceListener class has numerous properties you can set to control where the output goes. To get the application-wide ASPTraceListener class, pull it out of the Application bag state with the ASPTraceListener key. Figure 2 lists all the properties and how to set them. These properties are quite straightforward, but LaunchDebuggerOnAssert needs a little more explanation. If a debugger is attached to the application, no matter what machine the debugger is running on, the assertion will stop in the attached debugger. In the case where no debugger is attached, it gets a little more interesting. Since the machine running the application could not have anyone logged in, simply cranking up the debugger when an assertion triggers didn't seem like a healthy idea. It's bad enough having a message box in a random WindowStation you can't get at, but having the just-in-time (JIT) debugger sitting there is worse. Consequently, I made the decision to launch the debugger only on the local machine that made the page request. If the page request comes from the local machine, that means someone who is logged into the machine made the request and can respond to the JIT debugger.
Since I am talking about the JIT debugger (see Figure 3), I wanted to mention that I generally choose the Microsoft CLR Debugger when encountering a problem. It has all the same features of the Visual Studio .NET debugger, but uses quite a bit less memory. Of course, you can always start debugging with an existing instance of the Visual Studio .NET debugger and save some memory.
Figure 3 The JIT Debugger
The final feature of the Bugslayer assertion is the simple control that shows the output right on the page. To use the control in your applications, right-click on the Visual Studio .NET Toolbox window and choose Customize Toolbox. In the .NET Framework Components tab, click on the Browse button and add BUGSLAYER.DLL to your component list. Make sure to check the checkbox next to AssertControl. The AssertControl's glyph on the toolbox is the red circle with the X through it, like the MB_ICONERROR icon in MessageBox.
Once you've added the AssertControl to your toolbox, you can drop it on your page. When an assertion triggers, the ASPTraceListener class will hunt down the active page, and if that page has an AssertControl on it, you will see the assertion and stack walk. Figure 4 shows the example from the test program. The AssertControl is at the bottom of the screen and is the red text delineated with the two horizontal lines.
Figure 4 AssertControl Output
One final thing I want to mention is that the Bugslayer assertion code will also ensure that any calls through the System.Diagnostic.Trace.WriteXxx methods will route to the ASP .NET tracing mechanism. In doing my ASP .NET applications, I still use the Trace property from the System.Web.UI.Page for my tracing. With both tracing and assertions now going to the ASP .NET tracing, you have some cool features to help diagnose your problems. Now I'll discuss some of the implementation highlights as well as some of the trials and tribulations I went through in getting everything to work.
Since one of my main goals was to handle any call to Debug.Assert, that pretty much meant I was doing some sort of TraceListener class. As I mentioned earlier, I discussed TraceListeners in my February 2001 column, so I won't cover them again here. The big problem I had was figuring out a way to get the current System.Web.UI.Page object in order to do the output. My first scheme was to have a special class, AssertPage, that you would derive your pages from if you wanted to see assertion output on the page. That class has special assertion methods for doing the assertions and an abstract method you'd have to provide to see the output. Inside ASPTraceListener, I only did Debug. Assert output to the ASP .NET Trace object. While this first attempt worked, I got annoyed with it because I didn't like using the abstract method for each page. Additionally, I also wanted to see output from Debug.Assert on the page. I felt I could do better. One very odd problem I ran into was a bug in the Visual Studio .NET Web designer. In Beta 2, it will not load ASPX pages derived from abstract classes. Since my AssertPage was an abstract class, I didn't think many of you would ever use my assertion code because you'd have to manually build your ASPX pages!
I spent quite a bit of time in the debugger looking at various static objects to see which one might have the current page in it. The HttpContext object is static and can easily get to the current TraceContext through HttpContext.Current.Trace. Poking at the HttpContext.Current property, I saw that the HttpContext.Current.Handler derives from IHttpHandler. Since System.Web.UI.Page derives from the IHttpHandler interface, it is the handler for page requests. A quick check revealed that I could use the is operator to check that HttpContext.Current.Handler truly holds a System.Web.UI.Page object. With that, I had my means of finding the current page at any time, no matter where I was in the application. Armed with the current System.Web.UI.Page, I could then recurse through the controls on the page looking to see if the user dropped an AssertControl on it.
After getting the page object, I turned to getting the debugger started when an assertion triggers. With the System.Diagnostic.Debugger object, it's quite easy to determine if a debugger is attached. If one is, you can just call Debug.Break and you are all set. However, if a debugger is not attached, calling Debug.Break can sometimes bring down ASPNET_WP.EXE, which is certainly not helpful. I found the best thing to do was to call Debugger.Launch to get the debugger started. As I mentioned previously, just launching a debugger is not a good thing if no one is logged in. Since I decided to launch the debugger only if the page request came from the same machine, I needed a way to determine if the requester was on the local machine.
At first, I thought it was going to be simple to see if the request was local because I looked at HttpContext.Current.Request in the Watch window and noticed it had an IsLocal property that would do the trick. Unfortunately, IsLocal is private, so you can't access it from your code. Since the Request object has a UserHostAddress property to get the IP address, there was not a corresponding LocalHostAddress so I had to look at the LOCAL_ADDR server variable. If those two addresses are the same, you're looking at a local request. Of course, I checked for the loopback adapter address, 127.0.0.1, on both sides as well. Figure 5 contains the two main routines from ASPTraceListener, HandleOutput and IsRequestFromLocalMachine, which show all of the key implementation points I've talked about so far.
Another issue that caused me some problems was not a coding issue at all. I wanted to have the AssertControl show a representative icon in the toolbox so you could see at a glance if it was loaded for your use. While the documentation for Visual Studio .NET is excellent for a Beta 2 release, I never could find anything about how to get a glyph associated with a control. My first thought was that it might be a resource in the DLL. I looked at the Crystal Reports DLLs in the toolbox and I was surprised to find that the only traditional resource information they had was the version information. Scratching my head, I figured I was missing some sort of new attribute that would indicate the glyph to use. Whipping out ILDASM, I poked through various DLLs in the GUI with no luck. Since ILDASM is a round-trip disassembler, I unassembled the full Crystal Reports control to a file. When the disassembly was in progress, I saw some warning lines that indicated it had to extract some bitmaps.
That's when I realized what I was missing. The bitmaps were embedded managed resources, and a quick check of the resulting .IL file showed the .mresource directive on bitmaps. I figured all I needed to do was add a 16×16 bitmap with the same name as my control to my project and I'd be set. After a quick compile, I still didn't see a bitmap in the toolbox. Looking at the bitmap properties in the project, I noticed that the Build Action defaulted to Content. All I needed to do was change the Build Action to Embedded Resource and I saw my bitmap.
One massively cool thing with C# is the new doc comments. (See the tips at the end of this column for more doc comment ideas.) The compiler will extract these comments into XML files so you can always have up-to-the-minute documentation out of your code. If you look in the Framework directory, you'll notice that these XML files are part of the distribution, and are what IntelliSense® will use to help you program. It's an excellent feature of C#. Inexplicably, the Visual Basic® .NET and managed C++ languages don't have document comments, which is a shame.
I also ran into a small issue with the doc comment feature. I always build my projects to treat all warnings as errors. Unfortunately, the C# compiler reports a CS1596 (XML documentation not updated during this incremental rebuild), which stops the build. Since I really like the doc comments and the compiler is so fast, I have been turning off incremental building to avoid the error.
There you have it, a real assertion for ASP .NET Web applications. While there isn't that much code, I think it solves the problem presented by the DefaultTraceListener quite nicely. If you are looking for something to do in your copious free time, the AssertControl is very simple. A good exercise would be to extend the control so that it hides the call stack until you click a button in the AssertControl. Additionally, you can always add the ability to change output options on the fly to it as well. When you do download the code, please make sure to look at the README file so you don't run into problems building the sample.
It's nearing Fall and your fellow developers are clamoring to figure out the new Visual Studio .NET IDE, so they want to hear your tips! Send them to me at firstname.lastname@example.org so you don't disappoint them.
Tip 47 The Visual Studio .NET editor makes doing your C# doc comments a piece of cake. At the top of your classes, properties, or methods, type "///" and the editor will automatically set up the comments for you, including parameters and return blocks. This feature will save you hours of time.
Tip 48 Once you add your doc comments to your projects, go to the Tool menu and select Build Comment Web Pages. This tool will automatically run through your code and build a complete Web application for your commented code! It's a great way to build up documentation for your libraries and share it with others.
Send questions and comments for John to email@example.com.
| John Robbins is a cofounder of Wintellect, a software consulting, education, and development firm that specializes in programming for Windows and .NET. He is the author of Debugging Applications (Microsoft Press, 2000). You can contact John at http://www.wintellect.com. |
From the October 2001 issue of MSDN Magazine.