Code Diagnostics with Code Contracts in C# and Conditional Compilation
.NET Framework comes with a set of debugging tools. Particularly if you are using and advanced IDE such as Visual Studio, you can use debugger and other diagnostic tools available in the IDE. This debugging, however, is only applicable during the development of the application. Once the application is developed and shipped and then if an error occurs, it is not easy to diagnose and remove those errors via IDE debuggers. To handle such runtime application errors, special information is added with the code which can be later on used to diagnose and treat the error and bugs once the application is deployed. This chapter throws light on those techniques. Let us start this article with conditional compilation.
Conditional compilation, as the name suggests, refers to the compilation of certain area of code based on some condition. Conditional compilation is done via preprocessor directive #. The idea behind preprocessor directives is that they are compiled before the main compilation takes place. There are four preprocessor directives that are most commonly used for conditional compilation. They are #if, #else, #endif, #elif.
Basically advantage of conditional compilation is that you can compile source codes for different platforms, also you can compile your code for testing purposes or for deployment purposes. The idea is that when a compiler reaches #if statement, it checks whether a corresponding symbol has been defined or not. If symbol is not defined, that piece of code is ignored otherwise that piece of code is executed. The symbol is defined at the top of the file via #define followed by symbol name which is capital by convention. To define symbol for a complete assembly, a compilation switch is used. Have a look at the first example to further understand this concept.
Now come inside the main method, here we have first used preprocessor directive #if followed by symbol TESTING which means that if TESTING symbol has been defined in the file, compile this section of code. #else works as typical else statement which says that if TESTING is not defined in the file, execute the code after #else.
You can also use logical operators with preprocessor directives as we did next in our code. After #if we said that if TESTING symbol has been defined AND DEBUG has NOT been defined, then execute the following code. But since, we have both defined in our file, the code after else would be executed. You can see how we used the ‘&&’ and ‘!’ operators.
The output of the code in Example1 is as follows:
In Example1, we described that how we can use preprocessor directives to decide which piece of code should execute and which should not. We can do same with methods. For example, we can write the code in which a method is called only if a certain symbol has been defined in the file.
One way to do this is to wrap all the calls to the method in #if preprocessor directive. This is shown in Example2.
This technique works perfectly fine but the code us cumbersome to write. If you have multiple calls to the Print method in your code you will have to enclose every call within these preprocessor directives. However, there is another more efficient way to do this and that is why Conditional attribute. Our next example demonstrates that how conditional attribute can be used in such scenarios.
You can see that how helpful Conditional operators can be while you have multiple calls to a method that you want to execute conditionally. You should not that Conditional attribute is merely an instruction to the compiler during compile time and is totally ignored at runtime.
Debug & Trace classes are used to implement basic assertion and logging into the application. Both of these classes are static and their major difference lays in their usage with different build types. Debug class is used mostly with the debug build whereas the Trace class works with both the trace and debug build.
An important point here is that you have to use a Conditional [(“DEBUG”)] with all the methods of the Debug class and Conditional [(“TRACE”)] with all the methods of the Trace class. This means that in order for the calls to the Debug and Trace class, execute, you will have to include DEBUG and TRACE symbols in your file or assembly respectively. However, luckily for us, Visual Studio does this task for us and incorporate DEBUG symbol and TRACE symbol in debug configuration and release configuration for every project, respectively.
The common methods contained by both Debug and Trace classes are Write, WriteLine, and WriteIf methods. The output of these methods is displayed on the debugger’s output window. Have a look at the next example to see how these methods actually work.
You can see that the sum, subtraction and the values that we wrote using Debug and Trace methods have been displayed in the output window.
Trace class contains some additional methods such as TraceWarning, TraceInformation and TraceError which register themselves to the TraceListeners that are active. (TraceListeners have been explained in the later sections)
Understanding Fail & Assert methods
Debug and Trace classes contain TraceListeners and Listeners Collection respectively. When a Fail is called the information is sent to these listeners which in turn process the information and write them to the debug output and also display a message box with an error. This dialog box asks you to abort the abort, ignore or retry. If you select retry, it lets you attach the debugger which can be used to diagnose the problem.
Assert is similar to Fail and accepts two arguments. The first argument is of type bool and the second argument is the error message that you want to display in case if first argument is false. Think Assert method int terms of real life assertions. For example, we assert that pizza delivery should take thirty minutes. If this assertion proves correct and pizza is delivered in 30 minutes we are happy with the pizza delivery service but if pizza deiver takes more than 30 minutes we say that “sorry, we can’t wait this much” you have to improve this, this is a defect in your system.
Both Fail & Assert methods have been explained in our next example.
The dialogue on right is displayed by the Fail method in Example5 and the dialog on the left is displayed by the Assert method. You can see the error messages we passed at the start of both dialogs.
We have used the term TraceListeners many times in the last section. TraceListeners are used to process the information passed by the Fail, Assert and Write method of Debug and Trace classes. Basically Debug & Trace classes contain Listeners property which holds a collection of different types of static TraceListener instances. By default the Listeners collection contain only one listener DefaultTraceListener which writes the information passed by the Write, Fail and Assert method to the debugger’s output window and it also displays the dialog box as shown in the last example. You can remove the default listener if you want and add you custom listeners by inheriting from the TraceListener class or you can also use any of the built in listener classes. In our next example, we will demonstrate that how you can remove the default listener and add some of the other predefined listeners to the program. Have a look at our 6th example.
Next, we have same information that we had in Example5 but in that case, the result of the Fail and Assert methods were written to the debuggers output and displayed in the dialog box, since there was only one DefaulTraceListener. Now we have removed the default listener and have added two of our own. So this time when you run the code in Example6, you would see that the output of the Fail and Assert method would be displayed on the console and written in ‘mytrace.txt’ file located in your ‘D’ directory, since these are the two active listeners in the listener collection. The output of the code is as follows:
Also, you can go the ‘D’ directory and open “mytrace.txt” file; you would see the above output in that file too.
In the last section, we explained that how Assert method of both Debug and Trace classes can be used to check whether a certain condition is met in the program or not. In Example6, we make an assertion that the result of the condition ‘a’ is less than ‘b’ should evaluate to true. This is basically our assertion that if this condition is satisfied, program should execute normally otherwise it should invoke the debugger and should throw an exception in case of Debug and Trace releases respectively.
However, a new more robust concept was introduced in the .NET Framework 4 which is commonly referred to as code contracts. Code contract is basically a contract between the caller method and the called method which further substantiates the assertion principle implemented by calling Assert method or by throwing custom exceptions. There are two integral parts of this code contract: A precondition and a postcondition. Meeting precondition is the responsibility of the calling method and meeting postcondition is the responsibility of the called method. This would be explained with the help of our next example. Have a look at the 7th example of this article.
Here, first two lines are preconditions. They are specified by calling Requires method of the Contract class. Inside the Requires method, we have to specify the condition that must be met by the calling function before the function execution starts. The first precondition we have is that the List collection ‘numbers’ which is being passed to this method should not be null. Second precondition is that the ‘numbers’ collection should not be readable because in that case, item would not be removed.
Post conditions are specified by calling Ensures method of Contract class. Inside the Ensures parameter we have to specify the condition which must be met when this function completes execution. In our example, we specified that the ‘numbers’ collection should not contain the ‘num’ item, when this method completes execution.
An important point here about preconditions and postconditions is that both are static methods. Both of them have to be defined at the start of any method because these are the conditions that have to be followed while writing actual code. Major difference between preconditions and postconditions lay in their verification time. Preconditions are verified before function execution starts, and postconditions are verified when function execution completes.
Coming back towards our 7th example; here in the main method we created a List collection which we named numcoll and we initialized it with 5 numbers. We passed it to RemoveItem method along with a number which is contained by numcoll. The numcoll method would return true. The output of the code in Example7 would be as follows:
An interesting thing to note here is that the methods in Contract class are defined with a Conditional attribute which is [Conditional (“CONTRACTS_FULL”)]. It means that unless you define the “CONTRACT_FULL” symbol in your program, you cannot take advantage of Contract methods as they would not be executed. However, luckily enough for us, if you go the project properties in the visual studio and enable the contract checking checkbox in the Code Contracts tab, this CONTRACTS_FULL is automatically added to the assembly. But for Code Contract tab to appear in Visual Studio, you will first have to download and install Contracts tool. This tool is available at Microsoft DevLabs site at this link.
Conditional compilation, coupled with code contracts can save you lots of time while diagnosing your application’s error during debugging as well as when application is deployed. In this article, we explained most fundamental concepts of conditional compilation and code contracts. I would advise you to implement the examples in this article yourself and also modify their code and see the impact in the output. If you need any help regarding the discussed topic, leave a message in the comments sections and I will get back as soon as possible. Happy Coding!
|All times are GMT +5.5. The time now is 19:59.|