Schedule Time for Building Debugging Systems


As you're doing the design and initial scheduling for your project, make sure to add in time for building your debugging systems. You need to decide up front how you're going to implement your crash handlers, file data dumpers, and other tools you'll need to help you reproduce problems reported from the field. I've always liked to treat the error handling systems as if they were a product feature. That way, others in the company can see how you're going to handle bugs proactively when they come up.

When it comes to .NET development, choosing how to handle error conditions is simple: use exceptions. The beauty of .NET is that, unlike native code, there's a standard exception class, System.Exception, which all other exceptions derive from. The one drawback to .NET exceptions is that you still have to rely on the developer documenting those exceptions being thrown by the method because it is not done at the language or .NET runtime level. However, I'll discuss some analysis tools later in this chapter that can force developers to provide that documentation.

Build All Builds with Debugging Symbols

Some of the debugging system recommendations that I do make aren't that controversial. I've been harping on my first recommendation for years: build all builds, including release builds, with full debugging symbols. Debugging symbols are the data that let the debugger show you source and line information, variable names, and data type information for your program. All that information is stored in a Program Database (PDB) file associated with your modules. If you're paid by the hour, spending forever at the assembly language level could do wonders for paying your mortgage. Unfortunately, the rest of us don't have the luxury of infinite time, so speedily finding those bugs is a priority.

There's a bit of confusion when it comes to turning on PDB file creation, mainly because there are numerous extra options to the /DEBUG switch, and the documentation on those switches appears to be wrong. The documentation says that the /DEBUG, /DEBUG+, and /DEBUG:FULL switches all produce full debugging symbols and have the compiler produce a DebuggableAttribute on the assembly to inform the JIT compiler that debugging information is available. This allows the application to be debugged if it's started under the debugger or attached to by the debugger. The documentation also lists a different switch, /DEBUG:PDBONLY, which it says will not generate the DebuggableAttribute, which is better for release builds. Again, according to the documentation, the drawback is that specifying /DEBUG:PDBONLY means that you can debug the program only if you start it under the debugger. Given that the inability to do source debugging after attaching to an application makes debugging infinitely harder, my eyes almost popped out of my head when I read that. I determined from doing several experiments that using /DEBUG:PDBONLY allows you to both start and attach to applications to debug to your heart's content. The main difference is that /DEBUG:FULL turns off inlining, which is putting the code for a method inside another method instead of making a call to a function. Because inlining helps with performance, I set all my Release builds to /DEBUG:PDBONLY and my Debug builds to /DEBUG:FULL.

Turning on debug symbols for a release build is quite easy, mainly because it is turned on by default for assembly-based wizard-generated projects. An assembly-based project is one in which the output is a .NET assembly, such as a class library or console application. For a C# normal assembly-based project, right click the project in Solution Explorer and select Properties. In the Project window, click the Build tab. Set the Configuration drop-down list box to Release. At the bottom of the page, click Advanced to get to the Advanced Build Settings dialog box. Set the Debug Info combo box to pdbonly. For Debug builds, you will set it to full. Figure 2-1 shows the Advanced Build Settings dialog box all set up to properly build with debugging symbols. Of course, don't get me started about why this option is buried in such an out-of-the-way dialog box when it should be in the General section of the Build property page.

Figure 2-1. Generating debugging information for a normal C# assembly-based project


For reasons that I still can't quite fathom, the Microsoft Visual Basic assembly-based project property pages are different from the one for C# projects, but the compiler switch is still the same. I guess differences such as this, which make it more confusing for everyone, are the result of language teams that obviously don't speak to one another. Figure 2-2 shows setting a release build that produces full debug symbols for an assembly-based project in Visual Basic. Right-click the project in Solution Explorer and click Properties. In the Project window, click the Compile tab. Set the Configuration drop-down list box to Release. Near the top of the page, click Advanced Compile Options to get to the Advanced Compiler Settings dialog box. Set the Generate debug info combo box to pdb-only. For Debug builds, you will set it to full.

Figure 2-2. Generating debugging information for an assembly-based Visual Basic project


For Microsoft ASP.NET applications, the only time you can debug you page code is by setting compilation element, debug attribute to true in Web.config. By setting the debug attribute to true, you are telling the development environment to add the D switch to Aspnet_compiler.exe, which is what actually builds your pages. The D switch in turn calls the language compiler with the /DEBUG+ switch, which is the equivalent to /DEBUG:FULL. Additionally, using /D with Aspnet_compiler.exe turns off all optimizations because your language compiler is spawned with the /OPTIMIZE switch.

Since I'm talking about setting ASP.NET compiler options, it's a good time for me to mention how you can set additional compiler options for your ASP.NET applications other than setting the debug element to true. The barely discussed <compilers> element in Web.config is where you can configure the <compiler> element for each of the compilers you're going to use. This is where Aspnet_compiler.exe picks up the settings from when it calls the appropriate language compiler. The following shows how to set options for both C# and Visual Basic:

<configuration>   <system.codedom>   <compilers>       <compiler language="c#;cs;csharp"                 extension=".cs"                 type="Microsoft.CSharp.CSharpCodeProvider, System,                       Version=2.0.0.0, Cuture=neutral,                       PublicKeyToken=b77a5c561934e089"                 compilerOptions="/d:TRACE /warnaserror+ /checked+"                 warningLevel="4" />      <compiler language="VB"                      extension=".vb"                      compilerOptions="/d:Trace=true"                      type="Microsoft.VisualBasic.VBCodeProvider, SystemVersion=2.0.0.0,                            Cuture=neutral,  PublicKeyToken=b77a5c561934e089"                      compilerOptions="/d:TRACE /warnaserror+"     </compilers>   </system.codedom> </configuration>


If the thought of manually changing your project's settings for your 200 projects to build with debug symbols in addition to the rest of the proper build switches has you dreading the work, don't worrythere's hope. For Chapter 7, "Extending the Visual Studio IDE," I wrote an extremely cool add-in, SettingsMaster, that takes all the work out of changing project settings. SettingsMaster's defaults are to set up your projects by using the settings recommended in this chapter.

Treat Warnings as Errors

If you've written anything more than Hello World! in managed code, you've certainly noticed that the compilers don't let much slide as far as compiler errors are concerned. For those of you coming from a C++ background and who are new to .NET, you are probably amazed at how much tighter everything feels; in C++, you could cast values to almost anything, and the compiler would blindly go on its merry way. In addition to ensuring that data types are explicit, the managed code compilers can do much more to help you with errors if you let them. As usual, the trick to debugging smarter is not one big gesture, but taking advantage of lots of small steps along the way. Making your tools as smart as possible is one of those steps.

In the Visual Studio documentation, if you browse the Contents pane and navigate to Development Tools and Languages\Visual Studio\Visual C#\C# Reference\C# Compiler Options \C# Compiler Errors (or go to http://msdn2.microsoft.com/en-us/library/ms228296.aspx), you'll see all the compiler errors for C#. (The Visual Basic compiler errors are also included in the Index pane if you start typing BC20, but amazingly, the compiler errors aren't indexed in the Contents pane.) As you scroll down the list of C# errors, you'll notice that some say Compiler Warning and indicate a level, for example, Compiler Warning (level 3) CS0168. If you keep scrolling down the list, you'll find warning levels from 1 through 4. When you have a warning, the compiler indicates that the construct at that location in the source code is syntactically correct but might not be contextually correct.

A perfect example is CS0168, which is "The variable 'var' is declared but never used." If you've ever had to maintain code, nothing is more frustrating than spending 20 minutes pounding through a 300-line poorly written method looking for what uses a particular variable, only to discover that nothing does. One of my favorite warnings is CS1591, "Missing XML comment for publicly visible type or member 'Type_or_Member'." That wonderful warning ensures that you're documenting all your code!

Given that the compiler can inform you of all sorts of wonderful contextual problems such as this, doesn't it make sense to fix these problems? I don't like to call the problems warnings because they are really errors. If you've ever had the opportunity to learn about compiler construction, particularly parsing, you probably walked away with two thoughts: parsing is extremely difficult, and people who write compilers are different from the rest of us. (Whether that's a good different or bad different, I'll let you decide.) If the compiler writers go to tremendous trouble to report a warning, they are telling you something they obviously feel is quite important and is probably a bug. When a client asks us to help with a bug, the first thing we do is verify with them that the code compiles with no warnings. If it doesn't, I tell them that I'll be glad to help but not until their code compiles cleanly.

Fortunately, Visual Studio generates projects with the appropriate warning levels by default, so you shouldn't have to set the warning levels manually. If you're building your C# project manually, you'll want to set the /WARN switch to /WARN:4. For Visual Basic manual compiles, warnings are on by default, so you specifically have to turn them off.

Although the warning levels are appropriately set by Visual Studio, the default for treating warnings as errors is not set correctly. Cleanly compiling code is a great thing, so you'll want to get the /WARNASERROR+ switch set for both the C# and Visual Basic compilers. That way you can't even begin to start debugging until the code is perfect. For C# projects, navigate to the Build tab of the project's property page as discussed in the "Build All Builds with Debugging Symbols" section earlier in the chapter. In the "Treat Warnings as Errors" group box, select the All option. As usual, you'll want to do this for both Debug and Release builds. For Visual Basic projects, on the Compile tab of the project's property page, select the Treat All Warnings As Errors check box (near the bottom of the page).

For C# projects in particular, treating warnings as errors will stop the build on all sorts of problems that you would normally not halt the build, such as CS0649 ("Field 'field' is never assigned to, and will always have its default value 'value'"), which indicates that you have a class member that is uninitialized. However, other messages, such as CS1573 ("Parameter 'parameter' has no matching param tag in XML comment (but other parameters do)"), might seem so annoying that you'll be tempted to turn off treating warnings as errors. I strongly suggest that you don't.

In the case of CS1573, you're using the phenomenal /DOC switch to generate the XML documentation for your assembly. This is a warning that should be an error because if you're using XML documentation, someone else on your team is probably going to be reading that documentation, and if you aren't documenting everything you assume with parameters, or anything else for that matter, you're doing everyone on your team a disservice.

Both the C# and Visual Basic compilers have a switch you should never use: /NOWARN. It allows you to ignore a specific warning. The whole point of using /WARNASERROR+ is to have the compiler do as much heavy lifting as possible so that you don't have to waste those late hours debugging something when you could have been sleeping. Never do anything at run time that you can do at compile time.

Know Where Your Assemblies Load

In the mean world of native C++ development, you have to carefully watch where your DLLs load into memory. Because of the way native code works, there are many hard-coded addresses embedded into the actual assembly language generated by the compiler. In a perfect world, this allows Windows to map the code section into memory and all hard coded addresses are correct with no extra work required.

In an imperfect world, if your native DLL loads into an address different from the one it was compiled to assume, major problems occur. The resulting activity is called relocation when the operating system has to run through the assembly language code for the DLL and find all the hard-coded offsets and update them. If there were only four or five offsets, that would be very quick on modern computers. Unfortunately, the number is always much higher than you ever expect, and it's not uncommon to see 100,000 or more hard-coded offsets in native code. That causes a major slowdown in DLL (and so application) loading.

As the operating system is updating the addresses, it's changing the actual in-memory code pages for the DLL. That means that your process can't take advantage of tricks that the operating system plays to share those code pages in memory across all processes using that DLL. Because the memory is changed just for your process, you are charged for that memory, significantly increasing your overall memory usage.

The last problem is the worst for native code. You have no idea where that DLL loads if relocation occurs. Even two machines with identical hardware and drivers will load the DLL into different places. The end result is that if your code crashes in that DLL, the user will report a crash address, and you'll be left explaining to the boss that that address means nothing to you and you can't solve the problem. That's always a career-limiting move!

As you can see, controlling where a DLL loads into memory can have a major impact on memory consumption and performance of your applications. The good news is that it's not difficult to get right, as we'll see in a few paragraphs. Before I jump into discussing the addressing situation in .NET applications, this is a perfect spot to show you how easily you can see if you have relocations in your application. The first way is to turn on mixed mode (both managed and native debugging at the same time). (I'll discuss how to turn on mixed mode debugging in the "Mixed-Mode Debugging" section of Chapter 5, "Advanced Debugger Usage with Visual Studio.") Run your application in Visual Studio and display the Modules window, which is accessible from the Windows submenu of the Debug menu or by pressing Ctrl+Alt+U with the default keyboard mapping. If a module has been relocated, its icon is partially covered by a red ball with an exclamation point. Additionally, the load address for the module has an asterisk (*) after the address range. Figure 2-3 shows where DllA.dll and DllC.dll were relocated in the debugging session.

Figure 2-3. The Visual Studio debugger Modules window with relocated DLLs


The second way is to download the free Process Explorer, written by my good friend Mark Russinovich, from Sysinternals (www.sysinternals.com). Process Explorer, as its name implies, shows you all sorts of information about your processes, for example, loaded DLLs and all open handles. It's such a useful tool that if you don't have it on your machines right now, stop immediately and download it! Also, make sure to read Chapter 4, "Common .NET Debugging Questions" for additional hints and tricks you can use to make debugging easier with Process Explorer.

Seeing whether you have relocated DLLs is very easy. Just follow the next procedure. Figure 2-4 shows what it looks like to have relocated DLLs in a process.

1.

Start Process Explorer and select your process.

2.

Select View DLLs from the View menu.

3.

Select your process in the upper half of the main window.

Figure 2-4. Process Explorer showing relocated DLLs


If any DLLs show up highlighted in yellow, they have been relocated.

Another excellent tool that will show relocated DLLs with not only the relocated address but also the original address is ProcessSpy from Christophe Nasarre's excellent "Escape from DLL Hell with Custom Debugging and Instrumentation Tools and Utilities, Part 2" in the August 2002 edition of MSDN Magazine (http://msdn.microsoft.com/msdnmag/issues/02/08/escapefromdllhell/). Process Explorer and ProcessSpy are similar utilities, but ProcessSpy comes with source code so that you can see how all the magic happens.

At this point, you're probably thinking that since managed components are compiled to DLLs, you might want to set their load location also. This is also referred to as setting the base address or rebasing. If you've explored the compiler switches for the C# and Visual Basic compilers, you might have seen the /BASEADDRESS switch for setting the base address. Well, when it comes to managed code, things are quite a bit different. If you really look at a managed DLL with Link.exe DUMP, the Portable Executable (PE) dumper from Visual Studio C++, or with Matt Pietrek's much better PEDUMP (MSDN Magazine, February 2002, http://msdn.microsoft.com/msdnmag/issues/02/02/PE/default.aspx), you'll notice a single imported function, _CorDllMain from MScoree.dll, and a single relocation entry.

Thinking that there might be actual executable code in managed DLLs, I disassembled a few, and everything in the module code section looked like data. I looked a bit more and noticed something very interesting. The entry point of the module, which is the place where execution starts, happens to be the same address as the imported _CorDllMain. That helped confirm that there's no native executable code.

Even though you might think that you don't have to worry about the DLL base address because managed code does not have any actual CPU-specific assembly language in them by default, you still will want to do it. This is especially important if you are running NGen on your binaries and installing them in the Global Assembly Cache (GAC). NGen is the tool that will pre-Just-In-Time (JIT) compile your .NET Intermediate Language (IL) and save the assembly language as part of the binary. When you NGen a binary, you can pay the same performance hit that you would if it were a native C++ DLL. Even if you aren't running NGen on your assemblies, and you have the relocation, the operating system will still have to go down the extra code paths to do the relocation. In fact, two recent articles in MSDN Magazine by members of the .NET team specifically addressed the performance issues with relocations, and they are worth looking at because they discuss how the team found and addressed relocation errors. The first article is "Winning Forms: Practical Tips For Boosting The Performance Of Windows Forms Apps" by Milena Salman, the performance lead on the Windows Form team in the March 2006 issue of MSDN Magazine (http://msdn.microsoft.com/msdnmag/issues/06/03/WindowsFormsPerformance/). The second is the February 2006 CLR Inside Out column by Claudio Caldato, a Program Manager for Performance and Garbage Collection on the CLR Team (http://msdn.microsoft.com/msdnmag/issues/06/02/CLRInsideOut/).

There are two ways to rebase the DLLs in your application. The first method is to use the Rebase.exe utility that comes with Visual Studio. Rebase.exe has many different options, but your best bet is to call it using the /b command-line switch with the starting base address and place the appropriate DLLs on the command line. The good news is that once you do the rebasing, you'll almost never have to touch those DLLs again.

There's one small drawback to using Rebase.exe: if you've strongly named your DLL, as you always should, you can't use Rebase.exe. The same applies if you've digitally signed the DLL. The problem is that Rebase.exe works by physically changing the binary on the disk. When strongly named DLLs or digitally signed DLLs are physically changed, they can no longer be loaded and will cause an unhandled AssemblyLoadException in your application. If you do want to use Rebase.exe, you'll have to rely on delayed signing.

Table 2-1 shows a table from the Visual Studio documentation for rebasing your DLLs. As you can see, the recommended format is to use an alphabetical scheme. I generally follow this scheme because it's simple. The operating system DLLs load from 0x70000000 to 0x78000000, even on 64-bit operating systems, so using the range in Table 2-1 will keep you from conflicting with the operating system. Of course, you should always look in your application's address space by using Process Explorer or ProcessSpy to see whether any DLLs are already loaded at the address you want to use.

Table 2-1. DLL Rebasing Scheme

DLL first letter

Starting address

AC

0x60000000

DF

0x61000000

GI

0x62000000

JL

0x63000000

MO

0x64000000

PR

0x65000000

SU

0x66000000

VX

0x67000000

YZ

0x68000000


If you have four DLLs in your application, Apple.dll, Dumpling dll, Ginger.dll, and Gooseberries.dll, you run Rebase.exe three times to get all the DLLs rebased appropriately. The following three commands show how to run Rebase.exe with those DLLs:

REBASE /b 0x60000000 APPLE.DLL REBASE /b 0x61000000 DUMPLING.DLL REBASE /b 0x62000000 GINGER.DLL GOOSEBERRIES.DLL


If multiple DLLs are passed on the Rebase.exe command line, as shown here with Ginger.dll and Gooseberries.dll, Rebase.exe will rebase the DLLs so that they are loaded back to back starting at the specified starting address.

There's an easier way than wrestling with the Rebase.exe program. You'll change your build to use the /BASEADDRESS switch. For both C# and Visual Basic, you'll need to get to the Advanced Build Settings or Advanced Compiler Settings dialog boxes, respectively. Refer to the "Build All Builds with Debugging Symbols" section earlier in the chapter. Each of those dialog boxes has a DLL Base Address edit box in which you can manually type in the address you want. As always, set the same base address for both Debug and Release builds.

Although you can use Rebase.exe to automatically handle setting multiple DLL load addresses at a time, you have to be slightly more careful when setting the load address at link time. If you set the load addresses of multiple DLLs too close together, you'll see the relocated DLL in the debugger's Modules window. The trick is to set the load addresses far enough apart that you never have to worry about them after you set them.

Using the same DLLs from the Rebase.exe example, I'd set their load address to the following:

APPLE.DLL           0x60000000 DUMPLING.DLL        0x61000000 GINGER.DLL          0x62000000 GOOSEBERRIES.DLL    0x62100000


The important two DLLs are Ginger.dll and Gooseberries.dll because they begin with the same character. When that happens, I use the third-highest digit to differentiate the load addresses. If I were to add another DLL that started with G, its load address would be 0x62200000.

One of the big questions I get when I tell people to rebase their files is, "What files am I supposed to rebase?" The rule of thumb is simple: if you or someone on your team wrote the code, rebase it. Otherwise, leave it alone. If you're using third-party components, your binaries will have to fit around them.

Always Build with Code Analysis Turned On

Many years ago I read an old Digital Equipment Corporation manual that had the phrase Never do at run time what you can do at compile time in the introduction. That's something that's always stayed with me because it's such a salient point when it comes to software. Though the /WARNASERRORS switch is quite useful, as I have already mentioned, Microsoft developed an internal set of best practices, called the Design Guidelines, to ensure the best .NET code possible. Fortunately, Brad Abrams and Krzysztof Cwalina brought them out in a book, Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries (Addison-Wesley, 2005), which should be required reading for all .NET developers.

The one problem with any form of development guidelines is that unless you are going to manually review the code, there's no possible way for you to ensure that you're going to catch all the violations. That prompted the Common Language Runtime (CLR) team to invest in a tool that would try to automate finding as many of those errors as possible. Originally, this tool was released as FxCop, but it is now fully integrated into Visual Studio Team Developer Edition and Visual Studio Team Suite and has been renamed Code Analysis. The stand-alone FxCop is still available at http://www.gotdotnet.com/team/FxCop/ and I'll talk soon about why you still want to use it.

What's surprised me over the last few years is how few teams are running either Code Analysis or FxCop on their code. The main reason most teams don't run it is because the first time they run the analysis on their code, they are confronted with a large number of very picky naming convention and capitalization errors, so most folks immediately discount the tool. The idea behind the naming convention and capitalization rules is to ensure consistency across all .NET development, so the idea of the rule is good. And by tossing out the tool, you're missing a huge opportunity to catch many tough errors in your code that you'll have to debug at run time.

For example, look closely at the following snippet of code to see if you can see the error:

int ret = ExpandEnvironmentStrings ( environmentVariable ,                                      expandedVariable ,                                      expandedVariable.Capacity ) ; Debug.Assert ( ret != 0 , "ret != 0" ); if ( Marshal.GetLastWin32Error ( ) != 0 )


It's a very subtle error, and this is one I had in some of my code. Code Analysis immediately finds that the error is the fact that the code is relying on the last error value from Expand-EnvironmentStrings (the Windows API) but calling Debug.Assert in between the PInvoke call and the use of the last error value. Debug.Assert may cause a message box to appear, which could destroy the last error and lead to code that behaves differently between debug and release build. The specific error reported by Code Analysis is CA2122. Sharp-eyed readers might also notice the other Code Analysis error in the above code snippet: instead of calling the Windows API, Code Analysis reports that the code should use the Environment.ExpandEnvironmentVariables .NET method.

If that example doesn't convince you that Code Analysis is the cure to bad code, look at this code snippet and try to spot the error:

            someConnection = new SqlConnection ( connection );             someCommand = new SqlCommand ( );             someCommand.Connection = someConnection;             someCommand.CommandText = "SELECT AccountNumber FROM Users " +                "WHERE Username='" + name +                "' AND Password='" + password + "'";             someConnection.Open ( );             accountNumber = someCommand.ExecuteScalar ( );


At first glance you may not see it, but you're looking at a classic case of leaving yourself open to an SQL Injection Attack. Code Analysis reports this as a CA2100 error and even tells you exactly what's wrong in the code:

Microsoft.Security : Review if the query string "SELECT AccountNumber FROM Users WHERE Usern ame='____' AND Password='____'", passed to DbCommand.set_CommandText(String):Void in DemoCla ss.UnsafeQuery(String, String, String):Object, accepts any user input. If so, consider using  a parameterized Sql query instead of building up the query via string concatenations.


It's a great idea to read through all the rules that Code Analysis supports by searching the help index for CA1000 to access the list of rules. What's great about the documentation is that it shows you examples of each of the errors in addition to solid suggestions for fixing the errors.

Amazingly, for assembly-based projects, enabling Code Analysis is done the same way for C# as it is for Visual Basic. Navigate to the project's property sheet, click the Code Analysis tab, and then select the Enable Code Analysis (Defines CODE_ANALYSIS Constant) check box. This property page is where you can set warnings to errors and disable specific Code Analysis rules. For ASP.NET projects, you can turn on Code Analysis by selecting Code Analysis Configuration from the Website menu. Also, with all types of projects, you can right-click the project in Solution Explorer and select the option to run Code Analysis at any time.

My rule for Code Analysis best practices are that you always turn it on for both Debug and Release builds. That way it's always part of the compile, and you can fix any reported problems as soon as they occur. I also recommend that you never turn off any of the rules in Code Analysis on your deliverable modules/projects. I know it's tempting to turn them off, especially the first time you run Code Analysis, for the purpose of eliminating capitalization and naming errors, on a set of code that's never been run through Code Analysis before. Another reason to fix those errors is so that your code looks like and is named consistently with the Framework Class Library in addition to all the code that everyone else is writing.

My final recommendation when it comes to Code Analysis is that you always set all rules to report errors so that the build stops if there's any problem. If you leave the default, which is to show warnings of errors, you'll never pay attention to problems found by Code Analysis. As I mentioned in the "Treat Warnings as Errors" section earlier in this chapter, you have to make builds stop on problems to force developers to address them.

Of course, the second time I had to manually change the Debug and Release builds on a project to have all Code Analysis rules treated as errors, I realized that was a task that needed to be automated. As part of the SettingsMaster add-in I wrote in Chapter 7, "Extending the Visual Studio IDE," I made sure it had the functionality to easily control all your Code Analysis settings. If you use the SettingsMaster Update Solution toolbar button, SettingsMaster will set all Code Analysis rules to errors.

Stand-Alone FxCop

Although there's quite a bit of overlap between Code Analysis and the stand-alone FxCop, the main difference is that FxCop offers spelling rules. It will use the Microsoft Office spelling engine to do the work of ensuring that you're spelling all your variables and natural parts of names correctly. If you're spelling-challenged as I am, the stand-alone FxCop can keep you from looking like an idiot when people are using your public APIs.

Although you can manually add all of your unique spellings to the Office spelling dictionary, a better way to tell FxCop about your spellings is through the CustomDictionary.xml file. This file resides in the same directory as the .FxCop project you'll save with your assemblies loaded. Listing 2-1 shows the CustomDictionary.xml file used by my code, which you can find in the .\FxCop directory of the book's source code.

Listing 2-1. Example CustomDictionary.xml File

  <Dictionary>   <Words>    <Recognized>     <Word>Wintellect</Word>     <Word>Unboxing</Word>     <Word>cref</Word>     <Word>Visualizers</Word>     <Word>Perf</Word>     <Word>nologo</Word>     <Word>nosymlocals</Word>     <Word>pid</Word>     <Word>Hhc</Word>     <Word>Plugin</Word>     <Word>plugin</Word>     <Word>minidump</Word>     <Word>Automator</Word>     <Word>Validators</Word>     <Word>Visualizers</Word>     <Word>Visualizer</Word>     <Word>Switches</Word>    </Recognized>    <Deprecated/>    <Inappropriate/>   </Words>   <Acronyms>    <CasingExceptions>    </CasingExceptions>   </Acronyms>   </Dictionary>



Custom Code Analysis Rules

As soon as I saw Code Analysis, I immediately wanted to write some custom rules. In Chapter 8, "Writing Code Analysis Rules," I'll give you all the details, because rule development is undocumented. However, it's appropriate to discuss using the rules in this section.

I've provided three rules files, Wintellect.FxCop.DesignRules.DLL, Wintellect.FxCop.PerformanceRules.DLL, and Wintellect.FxCop.UsageRules.DLL. You can use the supplied WintellectToolsInstall.MSI to install my rules into your copy of Visual Studio Team Developer Edition or Visual Studio Team Suite Edition. Once you've installed my Code Analysis Rules, they automatically show up in the Code Analysis project properties. If you're using FxCop, my rules compiled against FxCop 1.35, the latest version at the time I wrote this, are in the .\FxCop directory where you installed the book's source code.

The rule in Wintellect.FxCop.PerformanceRules.DLL is AvoidBoxingAndUnboxingInLoops. The first thing everyone hears about .NET development is that you're supposed to avoid BOX and UNBOX instructions. A BOX instruction is generated when you are passing a value type to a method that takes only objects. An UNBOX instruction extracts the value type from the object. The problem with these two instructions is that they are very slow and put pressure on the managed heap, so you don't want them in your code.

Because it's so hard to spot the value types by code inspection, I guess you could disassemble all your binaries to Intermediate Language files and manually look for the offending instructions if they appear in a looping construct. It's completely impractical to manually look for these instructions, and having one inside a loop can destroy performance. Consequently, I wrote this rule to warn you if you have that condition in your code.

The Wintellect.FxCop.Design.DLL file contains numerous interesting rules. Although Code Analysis has a rule to ensure that you've set the assembly version number, I wanted a set of rules that would look for other key assembly attributes, such as the company name, copyright values, descriptions, and titles. Those rules aren't very exciting, but they ensure that your assemblies are identifiable.

The more interesting rules deal with XML Doc Comments. The first, AssembliesHaveXmlDocCommentFiles, ensures that you're building valid XML documentation files with your assemblies. The other two rules, ExceptionDocumentationInvalidRule and ExceptionDocumentation MissingRule, require a little background discussion as to why they are important.

The most important data in your XML Doc Comments are the exceptions directly thrown by a method. A direct throw is one that has a physical throw statement in the method. A caller is much more interested in potentially handling a direct throw in a method because they are specifically expected. Although other methods called by the target method could certainly throw exceptions from deep in a call chain, it's those direct throw statements that everyone's interested in and that everyone needs to see documented with the wonderful <exception> tag.

The ExceptionDocumentationMissingRule rule looks through the instructions in the method and finds all direct throws and compares them to the actual XML Doc Comment file. If the documentation is missing, a Code Analysis error is the result. ExceptionDocumentationInvalidRule is the opposite of ExceptionDocumentationMissingRule in that if it finds a documented exception that's not actually thrown, the rule generates the error.

The first two rules in Wintellect.FxCop.UsageRules.DLL are DoNotUseTraceAssertRule and CallAssertMethodsWithMessageParametersRule. As you can deduce from the rule names, they are to warn you when you're calling Trace.Assert and the Debug.Assert overload without a message parameter. In the Assertions in .NET section, I go through the numerous reasons why these two methods should be avoided.

In Chapter 24 of his CLR via C#, Second Edition (Microsoft Press, 2005), Jeffrey Richter goes through a litany of problems that can occur in your synchronization using SyncBlocks and Monitors. Jeffrey's rules were screaming to be put into Code Analysis rules, so I created the four rules DoNotLockOnPublicFields, DoNotLockOnThisOrMe, DoNotLockOnTypes, and DoNotUseMethodImplAttributeWithSynchronized to ensure that you have your synchronization implemented correctly.




Debugging Microsoft  .NET 2.0 Applications
Debugging Microsoft .NET 2.0 Applications
ISBN: 0735622027
EAN: 2147483647
Year: 2006
Pages: 99
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net