- C# 6 and .NET Core 1.0:Modern Cross:Platform Development
- Mark J. Price
- 2914字
- 2025-04-04 20:07:42
Debugging and diagnostics
In this section, you will learn how to debug problems at design time, trace problems at runtime, and use types such as Debug
, Trace
, Process
, and Stopwatch
that are in the System.Diagnostics
namespace.
Debugging an application
In Visual Studio, press Ctrl + Shift + N or navigate to File | New | Project….
In the New Project dialog, from the Installed Templates list, select Visual C#. In the list at the center, select Console Application, type the name Ch05_Debugging, change the location to C:\Code
, type the solution name Chapter05, and then click on OK.
Modify the template code to look like this:
using static System.Console; namespace Ch05_Debugging { class Program { static double Add(double a, double b) { return a * b; // deliberate bug! } static void Main(string[] args) { double a = 4.5; // or use var double b = 2.5; double answer = Add(a, b); WriteLine($"{a} + {b} = {answer}"); ReadLine(); // wait for user to press ENTER } } }
Press Ctrl + F5 and take a look at the output:
4.5 + 2.5 = 11.25
There is a bug! 4.5
added to 2.5
should be 7
and not 11.25
. We will use the debugging tools in Visual Studio 2015 to squash the bug.
Setting a breakpoint
Breakpoints allow us to mark a line of code that we want to pause at to find bugs. Click on the open curly bracket at the beginning of the Main
method and go to the Debug | Toggle Breakpoint menu, or press F9.
A red highlight will appear with a red circle in the grey margin bar on the left-hand side, to indicate that a breakpoint has been set. Breakpoints can be toggled off with the same command. You can also click in the margin to toggle the breakpoint on and off, as shown in the following screenshot:

Go to Debug | Start Debugging, or press the Start toolbar button, or press F5. Visual Studio starts and then pauses when it hits the breakpoint. This is known as break mode. The line that will be executed next is highlighted in yellow, and a yellow arrow points at the line from the grey margin bar, as shown in the following screenshot:

Tip
You can drag the yellow arrow and its highlight. When you continue executing, it will run from the new position. This is useful for moving back a few statements to rerun them or to skip over some statements.
The debugging toolbar
Visual Studio enables some extra toolbar buttons to make it easy to access debugging features. Here are a few of those:
- Continue / F5 (green triangle): This button will run the code at full speed from the current position
- Stop Debugging / Shift + F5 (red square): This button will stop the program
- Restart / Ctrl + Shift + F5 (circular black arrow): This button will stop and then immediately restart the program
- Step into / F11, Step over / F10, and Step out / Shift + F11 (blue arrows over dots): These buttons will step through the code in various ways
The following screenshot illustrates Visual Studio's extra toolbar buttons:

Debugging windows
Visual Studio makes some extra windows visible so that you can monitor useful information such as variables while you step through your code. If you cannot find one of these windows, then on the Debug menu, choose Windows, and then select the window you want to view.
Tip
Most of the debug windows are only available when you are in the Break mode.
The Locals window shows the name, value, and type for any local variables. Keep an eye on this window while you step through your code:

In Chapter 1, Hello, C#! Welcome, .NET Core!, I introduced you to the C# Interactive window. The similar, but more basic, Immediate Window also allows live interaction with your code.
For example, you can ask a question such as, "What is 1+2?" by typing ?1+2
and pressing Enter. You can also use the question mark to find out the current value of a variable:

You can execute statements of code:

As long as you have Visual Studio 2015 with Update 1, the C# Interactive window is better.
Stepping through code
From the Debug menu, choose Debug | Step Into, or click on the Step Into button in the toolbar, or press F11. The yellow highlight steps forward one line, as shown in the following screenshot:

Choose Debug | Step Over or press F10. The yellow highlight steps forward one line. At the moment, there is no difference between using Step Into or Step Over.
Press F10 again so that the yellow highlight is on the line that calls the Add
method:

The difference between Step Into or Step Over can be seen when you are about to execute a method call. If you press Step Into, the debugger steps into the method so that you can step through every line in that method. If you press Step Over, the whole method is executed in one go (it does not skip over the method!).
Use Step Into to step inside the method. Hover your mouse over the multiply (*
) operator. A tooltip will appear showing that this operator is multiplying a
by b
to give the result 11.25
. We can see that this is the bug. You can pin the tooltip by clicking on the pin icon as I have done here:

Fix the bug by changing the *
to +
.
We don't need to step through all the lines in the Add
method, so choose Step Out or press Shift + F11. Press F11 or choose Step Into to assign the return value of the Add
method to the variable answer
.
The Locals window highlights the most recent change in red text. The answer is correct, so choose Continue or press F5:

Customizing breakpoints
You can also right-click on a breakpoint and choose additional options, such as Conditions, as shown in the following screenshot:

The conditions for a breakpoint include an expression that must be true and a hit count to reach for the breakpoint to apply.
In the example, as you can see in the following screenshot, I have set a condition to only apply the breakpoint if both the answer
variable is greater than 9
and we have hit the breakpoint three times:

You have now fixed a bug using some of Visual Studio's debugging features.
Monitoring performance and resource usage
To write the best applications, we need to be able to monitor the speed and efficiency of our code.
Evaluating the efficiency of types
What is the best type to use for a particular scenario? To answer this question, we need to carefully consider what we mean by best. We should consider the following four factors:
- Functionality: This can be decided by checking whether the type provides the features you need
- Memory size: This can be decided by the number of bytes of memory the type takes up
- Performance: This can be decided by how fast the type is
- Future needs: This depends on the changes in requirements and maintainability
There will be scenarios, such as storing numbers, where multiple types have the same functionality, so we would need to consider the memory and performance in order to make a choice.
If we need to store millions of numbers, then the best type to use would be the one that requires the least number of bytes of memory. If we only need to store a few numbers but we need to perform lots of calculations on them, then the best type to use would be the one that runs fastest on a particular CPU.
You have seen the use of the sizeof()
operator to show the number of bytes a single instance of a type uses in memory. When we are storing lots of values in more complex data structures, such as arrays and lists, then we need a better way of measuring memory usage.
You can read lots of advice online and in books, but the only way to know for sure what the best type would be for your code is to compare the types yourself. In the next section, you will learn how to write the code to monitor the actual memory requirements and the actual performance when using different types.
Although today a short
variable might be the best choice, it might be a better choice to use an int
variable, even though it takes twice as much space in memory, because we might need a wider range of values to be stored in the future.
There is another metric we should consider: maintenance. This is a measure of how much effort another programmer would have to put in, to understand and modify your code. If you use a nonobvious type choice, it might confuse the programmer who comes along later and needs to fix a bug or add a feature. There are analyzing tools that will generate a report that shows how easily maintainable your code is.
Monitoring performance and memory use
The System.Diagnostics
namespace has lots of useful types for monitoring your code. The first one we will look at is the Stopwatch
type.
Add a new console application project named Ch05_Monitoring. Set the solution's start up project to be the current selection.
Modify the template code to look like this:
using System; using System.Diagnostics; using System.Linq; using static System.Console; using static System.Diagnostics.Process; namespace Ch05_Monitoring { class Recorder { static Stopwatch timer = new Stopwatch(); static long bytesPhysicalBefore = 0; static long bytesVirtualBefore = 0; public static void Start() { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); bytesPhysicalBefore = GetCurrentProcess().WorkingSet64; bytesVirtualBefore = GetCurrentProcess().VirtualMemorySize64; timer.Restart(); } public static void Stop() { timer.Stop(); long bytesPhysicalAfter = GetCurrentProcess().WorkingSet64; vlong bytesVirtualAfter = GetCurrentProcess().VirtualMemorySize64; WriteLine("Stopped recording."); WriteLine($"{bytesPhysicalAfter - bytesPhysicalBefore:N0} physical bytes used."); WriteLine($"{bytesVirtualAfter - bytesVirtualBefore:N0} virtual bytes used."); WriteLine($"{timer.Elapsed} time span ellapsed."); WriteLine($"{timer.ElapsedMilliseconds:N0} total milliseconds ellapsed."); } } class Program { static void Main(string[] args) { Write("Press ENTER to start the timer: "); ReadLine(); Recorder.Start(); int[] largeArrayOfInts = Enumerable.Range(1, 10000).ToArray(); Write("Press ENTER to stop the timer: "); ReadLine(); Recorder.Stop(); ReadLine(); } } }
Tip
The Start
method of the Recorder
class uses the garbage collector (GC) type to ensure that all the currently allocated memory is collected before recording the amount of used memory. This is an advanced technique that you should almost never use in production code.
You have created a class named Recorder
with two methods to start and stop recording the time and memory used by any code you run. The Main
method starts recording when the user presses Enter, creates an array of ten thousand int
variables, and then stops recording when the user presses Enter again.
The Stopwatch
type has some useful members, as shown in the following table:

The Process
type has some useful members:

Press Ctrl + F5 to start the application without the debugger attached. The application will start recording the time and memory used when you press Enter, and then stop recording when you press Enter again:
Press ENTER to start the timer: Press ENTER to stop the timer: Stopped recording. 942,080 physical bytes used. 0 virtual bytes used. 00:00:03.1166037 time span ellapsed. 3,116 total milliseconds ellapsed.
Measuring the efficiency of processing strings
Now that you've seen how the Stopwatch
and Process
types can be used to monitor your code, we will use them to evaluate the best way to process string
variables.
Add a new console application project named Ch05_BuildingStrings. Add the following using
statements:
using System; using System.Diagnostics; using System.Linq; using System.Text; using static System.Console; using static System.Diagnostics.Process;
Copy and paste the class definition for the Recorder
class from the earlier project.
Tip
Best Practice
Although copy and paste is a valid technique for code reuse in some scenarios, it would be better to create a class library assembly for the Recorder
class so that we can share it between multiple projects without maintaining multiple copies. You will learn how to do this in Chapter 6, Building Your Own Types with Object-Oriented Programming.
Add the following code to the Main
method. It creates an array of ten thousand int
variables and then concatenates them with commas for separators using a string
and a StringBuilder
:
int[] numbers = Enumerable.Range(1, 10000).ToArray(); Recorder.Start(); WriteLine("Using string"); string s = ""; for (int i = 0; i < numbers.Length; i++) { s += numbers[i] + ", "; } Recorder.Stop(); Recorder.Start(); WriteLine("Using StringBuilder"); StringBuilder builder = new StringBuilder(); for (int i = 0; i < numbers.Length; i++) { builder.Append(numbers[i]); builder.Append(", "); } Recorder.Stop(); ReadLine();
Press Ctrl + F5 to see the output:
Using string Stopped recording. 7,540,736 physical bytes used. 69,632 virtual bytes used. 00:00:00.0871730 time span ellapsed. 87 total milliseconds ellapsed. Using StringBuilder Stopped recording. 8,192 physical bytes used. 0 virtual bytes used. 00:00:00.0015680 time span ellapsed. 1 total milliseconds ellapsed.
We can summarize the results as follows:
- The
string
class used about 7.5 MB of memory and took 87 milliseconds - The
StringBuilder
class used 8 KB of memory and took 1.5 milliseconds
In this scenario, StringBuilder
is about one hundred times faster and about one thousand times more memory efficient when concatenating text!
Tip
Best Practice
Avoid using the String.Concat
method or the +
operator with string
variables. Instead, use StringBuilder
or the C# 6 $ string interpolation to concatenate variables together, especially inside loops.
Monitoring with Debug and Trace
You have seen the use of the Console
type and its WriteLine
method to provide output to the console window. We also have a pair of types named Debug
and Trace
that have more flexibility in where they write out to.
The Debug
and Trace
classes can write to any trace listener. A trace listener is a type that can be configured to write output anywhere you like when the Trace.WriteLine
method is called. There are several trace listeners provided by .NET, and you can even make your own by inheriting from the TraceListener
type.
Writing to the default trace listener
One, the DefaultTraceListener
, is configured automatically and writes to Visual Studio's output window; you can configure others manually using code or a configuration file.
Add a new console application project named Ch05_Tracing. Modify the template code to look like this:
using System.Diagnostics; using static System.Console; namespace Ch05_Tracing { class Program { static void Main(string[] args) { Debug.WriteLine("Debug says Hello C#!"); Trace.WriteLine("Trace says Hello C#!"); WriteLine("Press ENTER to close."); ReadLine(); } } }
Press F5 to start Visual Studio with the debugger attached. In Visual Studio's output window, you will see the two messages. If you cannot see the output window, press Ctrl + W, O or navigate to View | Output menu.
Ensure that you show output from Debug, as shown in the following screenshot:

Configuring trace listeners
Now, we will configure some trace listeners that will also write to a text file and to the Windows application event log.
In Visual Studio's Solution Explorer, double-click on the file named App.config
and modify it to look like this:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.diagnostics> <sharedListeners> <add name="file" type="System.Diagnostics.TextWriterTraceListener" initializeData="C:\Code\Trace.txt" /> <add name="appeventlog" type="System.Diagnostics.EventLogTraceListener" initializeData="Application" /> </sharedListeners> <trace autoflush="true"> <listeners> <add name="file" /> <add name="appeventlog" /> </listeners> </trace> </system.diagnostics> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" /> </startup> </configuration>
You have configured two shared listeners—one that writes to a text file and another that writes to the application event log.
Press F5 to start Visual Studio with the debugger attached. In the console application, press Enter to close it. This will release the file that it is writing to. Click on the Windows Start menu, type event, and then click on Event Viewer:

In the Event Viewer window, expand Windows Logs, choose Application, choose the most recent log entry, and then click on the Details tab. You should see that the Friendly View option of the EventData is the message we output:

Run File Explorer, look in the C:\Code
folder, and open the file named Trace.txt
. If you open it with Notepad, it will look like this:

Configuring compiler symbols for .NET Framework
You might be wondering what the difference between Debug
and Trace
is. When you compile and run any application, it can be configured with the debug or trace compiler symbols on or off. By default, both are enabled. You can see this by double-clicking on Properties in the Solution Explorer window, and then clicking on the Debug tab.
You can see that both the debug and trace symbols are enabled. You can define your own symbols by entering them in the Conditional compilation symbols box, as shown in the following screenshot, where I have defined two symbols named KERMIT and FOZZIE:

Defining compiler symbols for .NET Core
If you chose to create a Console Application (Package) project to target the .NET Core, then you must define compiler symbols using the project.json file.
In the project.json
file, add the configurations
section, as shown in the following code, that specifies options for the two possible solution configurations—Debug and Release:
{ "version": "1.0.0-*", "description": "Ch05_Tracing Console Application", "authors": [ "markjprice" ], "tags": [ "" ], "projectUrl": "", "licenseUrl": "", "configurations": { "Debug": { "compilationOptions": { "define": [ "DEBUG", "TRACE", "KERMIT", "FOZZIE" ] } }, "Release": { "compilationOptions": { "define": [ "RELEASE", "TRACE" ], "optimize": true } } },
Checking compiler symbols
Modify the content of the Main
method to look like this. We are using conditional compilation #if
statements to only write to the trace listeners if the KERMIT and FOZZIE symbols have been defined. Note that they are Booleans so we can use operators like AND
(&&
) on them:
namespace Ch05_Tracing { class Program { static void Main(string[] args) { Debug.WriteLine("Debug says Hello C#!"); Trace.WriteLine("Trace says Hello C#!"); #if KERMIT Trace.WriteLine("KERMIT is on!"); #endif #if KERMIT && FOZZIE Trace.WriteLine("KERMIT and FOZZIE are on!"); #endif
Press F5 to start Visual Studio with the debugger attached.
In Visual Studio's output window, you will see all the messages:
Debug says Hello C#! Trace says Hello C#! KERMIT is on! KERMIT and FOZZIE are on!
In Visual Studio's toolbar, go to the drop-down menu that shows the list of configurations and choose Release. In this configuration, only the TRACE
directive is set:

Press F5 to start Visual Studio with the debugger attached. If you see a warning message, choose Continue Debugging. In Visual Studio's Output window, you will see only the Trace message:

The idea is that you can safely put as many Debug.WriteLine
statements throughout your code, knowing that when you finally compile and deploy the release version of your application they will all be automatically removed.
Tip
Best Practice
Use Debug.WriteLine
statements liberally throughout your code, knowing that they will be stripped out automatically when you compile the release version of your application.
If you need more flexibility, then you can also define your own symbols, but for these, you must manually check that your own symbol has been defined using #if
statements.
But what about the Trace.WriteLine
statements? They are left in your release code, so they should be used more sparingly. Even these can be configured using trace switches.
Switching trace levels
In the App.config
file, add the following section inside <system.diagnostics>
. It can go before or after the <trace>
and <sharedListeners>
sections:
<switches> <add name="PacktSwitch" value="3"/> </switches>
The value of a switch can be set using a number or a word. For example, the number 3 can be replaced with the word Info, as shown in the following table:

In the Main
method, add the following statements before prompting the user to press Enter:
var ts = new TraceSwitch("PacktSwitch", ""); Trace.WriteLineIf(ts.TraceError, "TraceError"); Trace.WriteLineIf(ts.TraceWarning, "TraceWarning"); Trace.WriteLineIf(ts.TraceInfo, "TraceInfo"); Trace.WriteLineIf(ts.TraceVerbose, "TraceVerbose"); Trace.Close(); // release any file or database listeners WriteLine("Press ENTER to close."); ReadLine();
This code will check the value of the switch named PacktSwitch
and only output if the level has been set.
Tip
Best Practice
Call the Close
method of the Trace
type to release any locks that might be held after writing to a text file trace listener. This is necessary only if you are writing to listeners that are buffered or apply locking, such as files and databases. However, it doesn't hurt to do this every time.
Press F5 to start Visual Studio with the debugger attached. If you see a warning message, choose Continue Debugging. In Visual Studio's Output window, you will see only the Trace messages up to level 3 (Info):
