55 KiB
Implement the Visual Studio Code debugging tools for C#
Introduction
The faster you discover and identify bugs, the faster you can get your code stabilized and released. Visual Studio Code supports code debugging for C# and most other software development languages through the use of Extensions. Once you've learned to use Visual Studio Code's debug tools, you'll spend less time wondering why your code stopped working and more time developing great applications.
Suppose you're using Visual Studio Code to develop a C# console application. The primary purpose of the application is to process customer data based on business rules. You develop the application using a small sample data set and it runs without errors. However, when you run the code using the larger data set, your code produces some unexpected results. You've read through the code several times but it's difficult to find the errors in your logic. You've heard that Visual Studio Code has good debugger tools, but you've never had to use them. You can't waste any more time reading through the code. You decide that learning the debugger tools is your best chance for completing the project on time.
In this module, you learn how to effectively debug C# programs in Visual Studio Code using breakpoints and other debugging tools, such as resources in the RUN AND DEBUG view.
By the end of this module, you'll be able to configure and use the Visual Studio Code debugger tools for C#.
Learning objectives
In this module, you will:
- Configure the Visual Studio Code debugger for a C# program.
- Create breakpoints and step through your code to isolate issues.
- Inspect your program state at any execution step.
- Use the call stack to find the source of an exception.
Ultimately, you'll be able to isolate code bugs efficiently using the debugger
tools, and you won't need to rely on Console.WriteLine
anymore.
Examine the Visual Studio Code debugger interface
The Visual Studio Code user interface provides several ways to configure debug options and launch debug sessions.
Debug features in the Visual Studio Code user interface
Visual Studio Code includes several user interface features that will help you to configure, start, and manage debug sessions:
- Configure and launch the debugger: The Run menu and RUN AND DEBUG view can both be used to configure and launch debug sessions.
- Examine application state: The RUN AND DEBUG view includes a robust interface that exposes various aspects of your application state during a debug session.
- Runtime execution control: The Debug toolbar provides high-level runtime controls during code execution.
Note
This Unit introduces you to a lot of debugging tools and terminology. Please keep in mind that this is your first look at these tools, not your last. You'll have an opportunity to complete hands-on activities with most of these tools during this module. Try not to feel overwhelmed by the volume of information that's presented.
Run menu options
The Visual Studio Code Run menu provides easy access to some common run and debug commands.
The Run menu provides menu options that are grouped into six sections.
-
Start and stop applications. This section of the menu includes options for starting and stopping code execution, with and without the debugger attached.
-
Launch configurations. This section of the menu provides access to examine or create launch configurations.
-
Runtime control. This section of the menu enables the developer to control how they want to advance through the code. Controls are enabled when execution has paused during a debug session.
-
Set Breakpoints. This section of the menu enables the developer to set breakpoints on code lines. Code execution pauses on Breakpoints during a debug session.
-
Manage Breakpoints. This section of the menu enables the developer to manage breakpoints in bulk rather than individually.
-
Install Debuggers. This section of the menu opens the Visual Studio Code EXTENSIONS view filtered for code debuggers.
Run and Debug view user interface
The RUN AND DEBUG view provides access to runtime tools that can be invaluable during the debug process.
-
Run and Debug controls panel. Used to configure and start a debug session.
-
VARIABLES section. Used to view and manage variable state during a debug session.
-
WATCH section. Used to monitor variables or expressions. For example, you could configure an expression using one or more variables and watch it to see when a particular condition is met.
-
CALL STACK section. Used to keep track of the current point of execution within the running application, starting with the initial point of entry into the application. The call stack shows which method is currently being executed, as well as the method or methods in the execution path that led to the current point of execution (current line of code).
-
BREAKPOINTS section. Displays the current breakpoint settings.
-
Debug toolbar. Used to control code execution during the debug process. This toolbar is only displayed while the application is running.
-
Current execution step. Used to identify the current execution step by highlighting it in the Editor. In this case, the current execution step is a breakpoint (breakpoints are marked with a red dot to the left of the line number).
-
DEBUG CONSOLE. Used to display messages from the debugger. The DEBUG CONSOLE panel is the default console for console applications and is able to display output from
Console.WriteLine()
and relatedConsole
output methods.
Controls panel for the Run and Debug view
At the top of the RUN AND DEBUG view, you can find the launch controls:
-
Start debugging. This button (a green arrow) is used to start a debug session.
-
Launch configurations. This dropdown menu provides access to launch configurations. The selected option is displayed.
-
Open 'launch.json'. This button (a gear shape) can be used to open the
launch.json
file, where you can edit the launch configuration if needed. -
Views and More Actions. This button (an ellipsis) enables you to show/hide sections of the debug panel as well as the DEBUG CONSOLE panel.
Debug toolbar
The Debug toolbar provides execution controls while your application is running.
-
Pause/Continue. This button can be used to pause execution when the code is running and Continue when code execution has been paused.
-
Step Over. This button can be used to execute the next method as a single command without inspecting or following its component steps.
-
Step Into. This button can be used to enter the next method or code line and observe line-by-line execution steps.
-
Step Out. When inside a method, this button can be used to return to the earlier execution context by completing all remaining lines of the current method as though they were a single command.
-
Restart. This button can be used to terminate the current program execution and start debugging again using the current configuration.
-
Stop. This button can be used to terminate the current program execution.
In addition to six execution controls, the Debug toolbar provides a "handle" on the left side that enables the developer to reposition the toolbar, and a "More" dropdown on the right side that enables the developer to disconnect the debugger.
Note
You can use the settingdebug.toolBarLocation
to control the location of the debug toolbar. It can be floating (the default), docked to the RUN AND DEBUG view, or hidden. A floating debug toolbar can be dragged horizontally and down to the Editor area.
Recap
Here are a few important things to remember from this unit:
The Visual Studio Code user interface can be used to configure, start, and manage debug sessions. The launch.json file contains the launch configurations for your application.
- The Run menu provides easy access to common run and debug commands grouped into six sections.
- The RUN AND DEBUG view provides access to runtime tools, including the Run and Debug controls panel. The sections of the RUN AND DEBUG view are VARIABLES, WATCH, CALL STACK, and BREAKPOINTS.
- The Debug toolbar provides execution controls while your application is running such as pause/continue, step over, step into, step out, restart and stop.
- The DEBUG CONSOLE is used to display messages from the debugger. The DEBUG CONSOLE can also display console output from your application.
Exercise - Run code in the debug environment
The Visual Studio Code user interface enables developers to run their code in a debug environment. Support for debugging is provided by extensions, and for C# developers, debugger support is provided by the same extension that provides support for code development and IntelliSense.
Debugger and application interaction
A code debugger can be used to pause and resume code execution, examine variable state, and even change the values assigned to variables at runtime. You may be wondering, how can the debugger control and modify a running application? The short answer is, the debugger has access to the application's runtime environment and executable code.
Note
Debugger interaction with the runtime environment is an advanced topic. In addition, understanding how the debugger works behind the scenes isn't a requirement for using the debugger. However, the following description may satisfy your curiosity.
The Visual Studio Code debugger for C# uses the .NET Core runtime to launch and interact with an application. When you start the debugger, it creates a new instance of the runtime and runs the application within that instance. The runtime includes an application programming interface (API), which the debugger uses to attach to the running process (your application).
Once your application is running and the debugger is attached, the debugger communicates with the running process using the .NET Core runtime's debugging APIs and a standard debug protocol. The debugger can interact with the process (the application running within the .NET runtime instance) by setting breakpoints, stepping through code, and inspecting variables. Visual Studio Code's debugger interface enables you to navigate the source code, view call stacks, and evaluate expressions.
The most common way to specify a debug session is a launch configuration in the launch.json file. This approach is the default option enabled by the debugger tools. For example, if you create a C# console application and select Start Debugging from the Run menu, the debugger uses this approach to launch, attach to, and then interact with your application.
Create a new code project
The first step in learning the debugger tools is creating a code project that you can run in the debugger.
-
Open a new instance of Visual Studio Code.
-
On the File menu, select Open Folder.
-
On the Open Folder dialog, navigate to your folder.
-
On the Open Folder dialog, select New folder.
-
Name the new folder Debug101, and then select Select Folder.
-
On the Terminal menu, select New Terminal.
A .NET CLI command can be used to create a new console app.
-
At the TERMINAL panel command prompt, enter the following command:
dotnet new console
-
Close the TERMINAL panel.
Examine launch configurations for debugging
Visual Studio Code uses a launch configuration file to specify the application that runs in the debug environment.
If the Debug101 folder doesn't include a Debug101.sln file, select Program.cs
,
and then verify that a .sln file is created.
Opening a C# code file prompts the environment to check for project files. The .sln file is a solution file that is used by Visual Studio to manage projects and is usually created automatically when you create a new project in Visual Studio Code. The .sln file is used by the debugger to identify the project that should be run in the debug environment.
On the View menu, select Command Palette.
At the command prompt, enter .net: g and then select .NET: Generate Assets for Build and Debug
.
Notice the new .vscode
folder that has been added to your project folder.
The .vscode
folder contains files that are used to configure the debug
environment.
Expand the .vscode
folder, and then select the launch.json file.
Take a minute to examine the launch.json file.
The launch configurations file can include multiple configurations. Each configuration includes a collection of attributes that are used to define that configuration.
Notice that the prelaunchTask
attribute specifies a build task.
In the .vscode
folder, select tasks.json
.
Notice that the tasks.json file contains the build task for your code project.
Close the launch.json
and tasks.json
files.
You take a closer look at the launch configuration attributes later in this module.
Run your code from the Run menu
The Run menu in Visual Studio Code provides the option to run your code with or without the debugger.
Open the Program.cs file.
Replace the contents of your Program.cs file with the following code:
// This code uses a names array and corresponding methods to display
// greeting messages
string[] names = new string[] { "Sophia", "Andrew", "AllGreetings" };
string messageText = "";
foreach (string name in names) {
if (name == "Sophia")
messageText = SophiaMessage();
else if (name == "Andrew")
messageText = AndrewMessage();
else if (name == "AllGreetings")
messageText = SophiaMessage();
messageText = messageText + "\n\r" + AndrewMessage();
Console.WriteLine(messageText + "\n\r");
}
bool pauseCode = true;
while (pauseCode == true);
static string SophiaMessage() {
return "Hello, my name is Sophia.";
}
static string AndrewMessage() {
return "Hi, my name is Andrew. Good to meet you.";
}
On the File menu, select Save.
Open the Run menu.
Notice that the Run menu provides options for running your code with or without debugging.
On the Run menu, select Run Without Debugging
Notice that the DEBUG CONSOLE panel displays console output, and that the Debug toolbar displays execution controls.
The DEBUG CONSOLE panel should be displayed below the code Editor. By default, the Debug toolbar (the small toolbar displaying code execution controls) is located above the code Editor and horizontally centered on the Visual Studio Code window.
On the Debug toolbar, select Stop.
Start a debug session from the Run menu
The Run menu includes the option to start a debug session.
On the Run menu, select Start Debugging
Take a minute to review the messages displayed in the DEBUG CONSOLE panel.
The output from your application is the same as when you ran without debugging, but other messages related to preparing the debug environment are displayed.
Notice the messages about loading .NET resources and your Debug101 application.
The first two messages report loading the .NET Core library and then your Debug101 application.
Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\7.0.4\System.Private.CoreLib.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Loaded 'C:\Users\someuser\Desktop\Debug101\bin\Debug\net7.0\Debug101.dll'. Symbols loaded.
The debugger uses a special instance of the .NET runtime to control the execution of your application and evaluate application state.
On the Debug toolbar, select Stop.
Run your code from the Run and Debug view
The RUN AND DEBUG view in Visual Studio Code supports a rich debugging experience.
Switch to the RUN AND DEBUG view.
In the RUN AND DEBUG view, select Start Debugging.
The Start Debugging button is the green arrow on the control panel at the top of the view.
Notice that the DEBUG CONSOLE panel shows the same messages about configuring the debugger that were displayed when starting a debug process from the Run menu.
On the Debug toolbar, select Stop.
Examine the output from your application
Before closing the DEBUG CONSOLE panel, take a minute to review the output that your code produced.
Notice that Andrew's greeting message is repeated unexpectedly.
During the remainder of this module, you'll use the Visual Studio Code debugger tools to investigate coding issues.
Recap
Here are a few important things to remember from this unit:
- The Visual Studio Code debugger for C# uses the .NET Core runtime to launch and interact with an application.
- The Visual Studio Code Run menu has options to start an application with and without the debugger attached.
- The Debug toolbar includes a button to Stop a running process.
- The RUN AND DEBUG view includes an option to start debugging an application.
Examine breakpoint configuration options
Debuggers are used to help you to analyze your code and can be used to control your program's runtime execution. When you start the Visual Studio Code debugger, it immediately begins executing your code. Because your code executes in micro-seconds, effective code debugging depends on your ability to pause the program on any statement within your code. Breakpoints are used to specify where code execution pauses.
Set breakpoints
Visual Studio Code provides several ways to configure breakpoints in your code. For example:
- Code Editor: You can set a breakpoint in the Visual Studio Code Editor by clicking in the column to the left of a line number.
- Run menu: You can toggle a breakpoint on/off from the Run menu. The current code line in the Editor specifies where the Toggle Breakpoint action is applied.
When a breakpoint is set, a red circle is displayed to the left of the line number in the Editor. When you run your code in the debugger, execution pauses at the breakpoint.
Remove, disable, and enable breakpoints
After setting breakpoints in your application and using them to isolate an issue, you may want to remove or disable the breakpoints.
To remove a breakpoint, repeat the action used to set a breakpoint. For example, click the red circle to the left of the line number or use the toggle breakpoint option on the Run menu.
What if you want to keep a breakpoint location, but you don't want it to trigger during your next debug session? Visual Studio Code enables you to "disable" a breakpoint rather than removing it altogether. To disable an active breakpoint, right-click the red dot to the left of the line number, and then select Disable Breakpoint from the context menu.
When you disable a breakpoint, the red dot to the left of the line number is changed to a grey dot.
Note
The context menu that appears when you right-click a breakpoint also includes the options to Remove Breakpoint (Delete) and Edit Breakpoint. The Edit Breakpoint option is examined in the Conditional breakpoints Logpoints section later in this unit.
In addition to managing individual breakpoints in the Editor, the Run menu provides options for performing bulk operations that act on all breakpoints:
- Enable All Breakpoints: Use this option to enable all disabled breakpoints.
- Disable All Breakpoints: Use this option to disable all breakpoints.
- Remove All Breakpoints: Use this option to remove all breakpoints (both enabled and disabled breakpoints are removed).
Conditional breakpoints
A conditional breakpoint is a special type of breakpoint that only triggers
when a specified condition is met. For example, you can create a conditional
breakpoint that pauses execution when a variable named numItems
is greater
than 5.
You've already seen that right-clicking a breakpoint opens a context menu that includes the Edit Breakpoint option. Selecting Edit Breakpoint enables you to change a standard breakpoint into a conditional breakpoint.
In addition to editing an existing breakpoint, you can also set a conditional breakpoint directly. If you right-click (rather than left-click) to set a new breakpoint, you can choose to create a conditional breakpoint.
When you create a conditional breakpoint, you need to specify an expression that represents the condition.
Each time the debugger encounters the conditional breakpoint, it evaluates the
expression. If the expression evaluates as true
, the breakpoint is triggered
and execution pauses. If the expression evaluates as false
, execution
continues as if there was no breakpoint.
For example, suppose you need to debug some code that's inside the code block
of a for
loop. You've noticed that the issue you're debugging only occurs
after the loop has completed several iterations. You decide that you want the
breakpoint to trigger once the loop's iteration control variable, i
, is
greater than three. You create a conditional breakpoint and specify the
expression i > 3
.
When you run your code in the debugger, it skips over your breakpoint until the
iteration when i > 3
evaluates as true. When i = 4
, execution pauses on
your conditional breakpoint.
Support for Hit Count
breakpoints and Logpoints
The C# debugger for Visual Studio Code also supports Hit Count
breakpoints
and Logpoints
.
A 'hit count' breakpoint can be used to specify the number of times that a breakpoint must be encountered before it will 'break' execution. You can specify a hit count value when creating a new breakpoint (with the Add Conditional Breakpoint action) or when modifying an existing one (with the Edit Condition action). In both cases, an inline text box with a dropdown menu opens where you can enter the hit count value.
A 'Logpoint' is a variant of a breakpoint that does not "break" into the
debugger but instead logs a message to the console. Logpoints are especially
useful for injecting logging while debugging production environments that
cannot be paused or stopped. A Logpoint is represented by a "diamond" shaped
icon rather than a filled circle. Log messages are plain text but can include
expressions to be evaluated within curly braces {}
Logpoints can include a conditional 'expression' and/or 'hit count' to further
control when logging messages are generated. For example, you can combine a
Logpoint message of i = {i}
with Hit Count condition >4
to generate log
messages as follows:
Recap
Here are a few important things to remember from this unit:
- Visual Studio Code enables setting breakpoints in the code editor or from the Run menu. Breakpoint code lines are marked with a red dot to the left of the line number.
- Breakpoints can be removed or disabled using the same options used to set them. Bulk operations that affect all breakpoints are available on the Run menu.
- Conditional breakpoints can be used to pause execution when a specified condition is met or when a 'hit count' is reached.
- Logpoints can be used to log information to the terminal without pausing execution or inserting code.
Exercise
Set breakpoints
Breakpoints are used during the debug process pause execution. This enables you to track variables and examine the sequence in which your code is executed. Breakpoints are a great way to start your debug process.
Set a breakpoint
Earlier in this module you completed an exercise where you ran an application in the debugger. The application displayed "greeting messages" in the DEBUG CONSOLE panel. At the end of the exercise, you noticed that the code repeats Andrew's greeting in an unexpected way.
In this exercise, you'll use a breakpoint to help you identify the issue.
Ensure that your Program.cs file contains the following code sample:
//This code uses a names array and corresponding methods to display
//greeting messages
string[] names = new string[] { "Sophia", "Andrew", "AllGreetings" };
string msg_txt = "";
foreach (string name in names) {
if (name == "Sophia") {
msg_txt = sophia_msg();
} else if (name == "Andrew") {
msg_txt = andrew_msg();
} else if { (name == "AllGreetings")
msg_txt = sophia_msg();
msg_txt = msg_txt + "\n\r" + andrew_msg();
}
Console.WriteLine(msg_txt + "\n\r");
}
bool pause_code = true;
while (pause_code == true);
static string sophia_msg() {
return "Hello, my name is Sophia.";
}
static string andrew_msg() {
return "Hi, my name is Andrew. Good to meet you.";
}
Use the Visual Studio Code debugger tools to set a breakpoint on the first code line inside the foreach loop.
Tip
One easy option for toggling on/off a breakpoint is to select (left-click) the area to the left of the line number. Breakpoints can also be set by using the Run menu and by using keyboard shortcuts.
On the Run menu, select Start Debugging.
Notice that code execution pauses at the breakpoint, and that the current code line is highlighted in the Editor.
On the Debug controls toolbar, select Step Into.
You can hover the mouse pointer over the buttons on the Debug controls toolbar to display the button labels.
Notice that code execution advances to the following code line and pauses:
messageText = SophiaMessage();
This code line assigns the return value of the sophia_msg
method to the
string variable messageText
.
Take a moment to consider why selecting Step Into produced this result.
- The Step Into button is used to advance to the next executable statement.
- Since the first element in the
names
array isSophia
and theif
statement is checking for the nameSophia
, the expression evaluates totrue
and code execution moves into the code block of theif
statement.
On the Debug controls toolbar, select Step Into.
Notice that code execution advances to the sophia_msg
method and pauses.
The Step Into button has advanced to the next executable code line. The
next executable code line isn't the next line number in the file, it's the next
statement in the execution path. In this case, the next executable statement is
the entry point to the sophia_msg
method.
On the Debug controls toolbar, select Step Out.
Notice that code execution returns to the code line that called the
sophia_msg
method and pauses.
Take a moment to consider why selecting Step Out produced this result.
When inside a method, the Step Out button completes the remaining lines of the current method and then returns to the execution context that invoked the method.
On the Debug controls toolbar, select Step Into.
Notice that code execution advances to the following code line and pauses:
messageText = messageText + "\n\r" + AndrewMessage();
Take a moment to consider why execution advanced to this code line.
Although the code indentation implies that this code line is part of the code
block for the else if
statement, it isn't. Using curly braces {}
to define
the code blocks for this if - else if
structure would have helped to avoid
this bug. As the code is written, Andrew's message will be added to msg_txt
each time the loop iterates.
Verify your code updates
Once you've isolated an issue in your code, you should update your code and then verify that the issue has been fixed.
On the Debug controls toolbar, select Stop.
Take a minute to fix your code logic.
You have a few options for fixing the identified issue in your code. For example:
-
You could keep the existing code lines and add curly braces
{}
to theif
structure for each code block. -
You could merge the two code lines that follow the final
else if
statement, forming a single statement as follows:
} else if (name == "AllGreetings") {
msg_txt = sophia_msg() + "\n\r" + andrew_msg();
}
Either way, your updated code must include the call to andrew_msg
within the
code block when name == "AllGreetings"
.
On the File menu, select Save.
Use the debugger UI tools to clear the breakpoint that you set earlier.
On the Run menu, select Start Debugging.
Verify that your code now produces the expected results.
Hello, my name is Sophia.
Hi, my name is Andrew. Good to meet you.
Hello, my name is Sophia.
Hi, my name is Andrew. Good to meet you.
On the Debug controls toolbar, select Stop.
Congratulations! You've successfully used the Visual Studio Code debugger to help you isolate and correct a logic issue.
Recap
Here are a few important things to remember from this unit:
- Use breakpoints to pause code execution during a debug session.
- Use Step Into from the Debug controls toolbar to observe the next executable code line.
- Use Step Out from the Debug controls toolbar to advance through the current method and back to the code line that called the method.
Examine the launch configurations file
You've already seen that Visual Studio Code uses the launch.json file to configure the debugger. If you're creating a simple C# console application, it's likely that Visual Studio Code generates a launch.json file that has all of the information you need to successfully debug your code. However, there are cases when you need to modify a launch configuration, so it's important to understand the attributes of a launch configuration.
Attributes of a launch configuration
The launch.json
file includes one or more launch configurations
in the
configurations list. The launch configurations use attributes to support
different debugging scenarios. The following attributes are mandatory for every
launch configuration:
name
: The reader-friendly name assigned to the launch configuration.type
: The type of debugger to use for the launch configuration.request
: The request type of the launch configuration.
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net8.0/ejm_debug.dll",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}
This section defines some of the attributes you may encounter.
Name
The name
attribute specifies the display name for the launch configuration.
The value assigned to name
appears in the launch configurations dropdown (on
the controls panel at the top of the RUN AND DEBUG view).
Type
The type
attribute specifies the type of debugger to use for the launch
configuration. A value of codeclr
specifies the debugger type for .NET 5+ and
.NET Core applications (including C# applications).
Request
The request
attribute specifies the request type for the launch configuration.
Currently, the values launch
and attach
are supported.
PreLaunchTask
The preLaunchTask
attribute specifies a task to run before debugging your
program. The task itself can be found in the tasks.json
file, which is in the
.vscode
folder along with the launch.json
file. Specifying a prelaunch task
of build
runs a dotnet build
command before launching the application.
Program
The program
attribute is set to the path of the application dll or .NET Core
host executable to launch.
This property normally takes the form:
${workspaceFolder}/bin/Debug/<target-framework>/<project-name.dll>
.
Where:
<target-framework>
is the framework that the debug project is being built for. This value is normally found in the project file as the 'TargetFramework' property.<project-name.dll>
is the name of debugged project's build output dll. This property is normally the same as the project file name but with a '.dll' extension.
For example: ${workspaceFolder}/bin/Debug/net7.0/Debug101.dll
Note
The .dll extension indicates that this file is a dynamic link library (dll) file. If your project is named Debug101, a file named Debug101.dll is created when a build task compiles your program using the Program.cs and Debug101.csproj files. You can find the Debug101.dll file in the EXPLORER view by expanding the "bin" and "Debug" folders, and then opening a folder that represents the .NET framework used by your code project, such as "net7.0". The .NET Framework version is specified in your .csproj file.
Cwd
The cwd
attribute specifies the working directory of the target process.
Args
The args
attribute specifies the arguments that are passed to your program at
launch. There are no arguments by default.
Console
The console
attribute specifies the type of console that's used when the
application is launched. The options are internalConsole
,
integratedTerminal
, and externalTerminal
. The default setting is
internalConsole
. The console types are defined as:
- The
internalConsole
setting corresponds to the DEBUG CONSOLE panel in the Panels area below the Visual Studio Code Editor. - The
integratedTerminal
setting corresponds to the OUTPUT panel in the Panels area below the Visual Studio Code Editor. - The
externalTerminal
setting corresponds to an external terminal window. The Command Prompt application that comes with Windows is an example of a terminal window.
Important
The DEBUG CONSOLE panel doesn't support console input. For example, the DEBUG CONSOLE can't be used if the application includes aConsole.ReadLine()
statement. When you're working on a C# console application that reads user input, theconsole
setting must be set to eitherintegratedTerminal
orexternalTerminal
. Console applications that write to the console, but don't read input from the console, can use any of the three console settings.
Stop at Entry
If you need to stop at the entry point of the target, you can optionally set
stopAtEntry
to be true
.
Edit a launch configuration
There are lots of scenarios when you might need to customize the launch configuration file. Many of those scenarios involve advanced or complex project scenarios. This module focuses on two simple scenarios when updating the launch configuration file is required:
- Your C# console application reads input from the console.
- Your project workspace includes more than one application.
Update the launch configuration to accommodate console input
As you read earlier, the DEBUG CONSOLE panel doesn't support console input. If
you're debugging a console application that relies on user input, you need to
update the console
attribute in the associated launch configuration.
To edit the console
attribute:
Open the launch.json
file in the Visual Studio Code Editor.
Locate the console attribute.
Select the colon and assigned value, and then enter a colon character.
Notice that when you overwrite the existing information with a colon, Visual Studio Code IntelliSense displays the three options in a dropdown list.
Select either integratedTerminal
or externalTerminal
.
Save the launch.json
file.
Update the launch configuration to accommodate multiple applications
If your workspace has only one launchable project, the C# extension will
automatically generate the launch.json
file. If you have more than one
launchable project, then you need to modify your launch.json
file manually.
Visual Studio Code generates a launch.json
file using the basic template that
you can update. In this scenario, you create separate configurations for each
application that you want to debug. Prelaunch tasks, such as a build task, can
be created in the tasks.json
file.
Suppose that you're working on a coding project that includes several console applications. The root project folder, SpecialProjects, is the workspace folder that you open in Visual Studio Code when you work on your code. You have two applications that you're developing, Project123 and Project456. You use the RUN AND DEBUG view to debug the applications. You want to select the application that you're debugging from the user interface. You also want any saved code updates to be compiled prior to attaching the debugger to your application.
You can achieve the requirements for this scenario by updating the launch.json
and tasks.json
files.
The following screenshot shows the EXPLORER view and the folder structure containing Project123 and Project456.
Notice that the name
, preLaunchTask
, and program
fields are all
configured for a specific application.
The name
attribute specifies the selectable launch option that's displayed in
the RUN AND DEBUG view user interface, the program
attribute specifies the
path to your application. The preLaunchTask
attribute is used to specify the
name of the task that's performed prior to launching the debugger. The
tasks.json
file contains the named tasks and the information required to
complete the task.
The following example shows how you could configure the tasks.json file. In this case, the named tasks specify build operations that are specific to the "Project123" and "Project456" applications. The build task ensures that any saved edits are compiled and represented in the corresponding .dll file that's attached to the debugger.
"version": "2.0.0",
"tasks": [
{
"label": "buildProject123",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Project123/Project123.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "buildProject456",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Project456/Project456.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
With your updates to the launch.json and tasks.json files in place, the RUN AND DEBUG view displays launch options for debugging either the Project123 or Project456 application. The following screenshot shows the names of the launch configurations displayed in the launch configuration dropdown:
Recap
Here are two important things to remember from this unit:
- Launch configurations are used to specify attributes such as
name
,type
,request
,preLaunchTask
,program
, andconsole
. - Developers can edit a launch configuration to accommodate project requirements.
Configure conditional breakpoints in C#
The C# debugger for Visual Studio Code supports the option to configure a breakpoint that only triggers if a condition is met. This type of breakpoint is called a conditional breakpoint. Conditional breakpoints can be configured directly or by editing an existing breakpoint.
Note
Visual Studio Code also supports a conditional breakpoint that triggers based on the number of times the breakpoint has been "hit".
Suppose you're debugging an application that processes product information in a
multidimensional string array. The array includes thousands of data points. The
problem that you're debugging seems to occur for products that are marked as
new. Your code processes the array inside a for
loop. You need to set a
breakpoint inside the loop, but you only want to pause when products are new
.
Use a standard breakpoint to examine a data processing application
Replace the contents of your Program.cs file with the following code:
int productCount = 2000;
string[,] products = new string[productCount, 2];
LoadProducts(products, productCount);
for (int i = 0; i < productCount; i++) {
string result;
result = Process1(products, i);
if (result != "obsolete") {
result = Process2(products, i);
}
}
bool pauseCode = true;
while (pauseCode == true) ;
This code uses a method named LoadProducts
to load data into the products
array. After the data is loaded, the code iterates through the array and calls
methods named Process1
and Process2
.
To generate data for the simulated processes, add the following method to the end of your Program.cs file:
static void LoadProducts(string[,] products, int productCount) {
Random rand = new Random();
for (int i = 0; i < productCount; i++) {
int num1 = rand.Next(1, 10000) + 10000;
int num2 = rand.Next(1, 101);
string prodID = num1.ToString();
if (num2 < 91) {
products[i, 1] = "existing";
} else if (num2 == 91) {
products[i, 1] = "new";
prodID = prodID + "-n";
} else {
products[i, 1] = "obsolete";
prodID = prodID + "-0";
}
products[i, 0] = prodID;
}
}
The LoadProducts
method generates 2000 random product IDs and assigns a value
of existing
, new
, or obsolete
to a product description field. There is
about a 1% chance that the products are marked new
.
To simulate data processing, add the following methods to the end of your Program.cs file:
static string Process1(string[,] products, int item) {
Console.WriteLine(
$"Process1 message - working on {products[item, 1]} product"
);
return products[item, 1];
}
static string Process2(string[,] products, int item) {
Console.WriteLine(
$"Process2 message - working on product ID #: {products[item, 0]}"
);
if (products[item, 1] == "new") {
Process3(products, item);
}
return "continue";
}
static void Process3(string[,] products, int item) {
Console.WriteLine(
$"Process3 message - processing product information for 'new' product"
);
}
The Process1
and Process2
methods display progress messages and return a
string.
Notice that the Process2
method calls Process3
if the product is new
.
On the Visual Studio Code File menu, select Save.
Near the top of the Program.cs file, set a breakpoint on the following code line:
result = Process1(products, i);
Open the RUN AND DEBUG view, and then select Start Debugging.
Use Step Into to walk through the code for Process1
and Process2
.
Notice the updates to the VARIABLES and CALL STACK sections of the RUN AND DEBUG view.
Continue to use Step Into to walk through the code until you see that i
is
equal to 3.
The VARIABLES section of the RUN AND DEBUG view displays the value assigned to
i
.
Notice that Process1
and Process2
display messages to the DEBUG CONSOLE
panel. A real application may require user interactions as data is being
processed. Some interactions may be dependent on the data being processed.
Use the Stop button to stop code execution.
Configure a conditional breakpoint using an expression
A standard breakpoint is great for walking through a data processing
application. However, in this case you're interested in new
products and you
don't want to walk through the analysis of each product to find the ones that
are new
. This scenario is a good example of when conditional breakpoints
should be used.
Right-click your existing breakpoint, and then select Edit Breakpoint
.
Enter the following expression:
products[i,1] == "new";
Notice that the expression is no longer displayed after you press Enter.
To display the expression temporarily, hover the mouse pointer over the breakpoint (red dot).
To run your application with the conditional breakpoint configured, select Start Debugging.
Wait for the application to pause at the conditional breakpoint.
Notice the value of i
displayed under the VARIABLES section.
On the Debug controls toolbar, select Continue
Notice that the value of i
has been updated the VARIABLES section.
Select Step Into.
Continue selecting Step Into until the Process1
message is displayed.
Notice that Process1
reports that it's working on a new product.
Take a moment to consider the advantage that conditional breakpoints offer.
In this simulated data processing scenario, there is about a 1% chance that a
product is new
. If you're using a standard breakpoint to debug the issue,
you'd need to walk through the analysis of about 100 products to find one of
the new
products that you're interested in.
Conditional breakpoints can save you lots of time when you're debugging an application.
Use the Stop button to stop code execution.
Congratulations! You successfully configured a conditional breakpoint.
Recap
Here are two important things to remember from this unit:
- Use a standard breakpoint to pause an application each time a breakpoint is encountered.
- Use a conditional breakpoint to pause an application when a Boolean
expression evaluates to
true
.
Exercise - Monitor variables and execution flow
The RUN AND DEBUG view provides developers with an easy way to monitor variables and expressions, observe execution flow, and manage breakpoints during the debug process.
Examine the sections of the Run and Debug view Each section of the RUN AND DEBUG view provides unique capabilities. Using a combination of these sections during the debug process is often helpful.
VARIABLES section
Monitoring variable state is an important aspect of code debugging. Unexpected changes in variable state will often help to identify logic errors in your code.
The VARIABLES section organizes your variables by scope. The Locals
scope
displays the variables in the current scope (the current method).
Note
The top-level statements section of a console application is considered its own method. A method namedMain
.
You can unfold (expand) the displayed scopes by selecting the arrow to the left
of the scope name. You can also unfold variables and objects. The following
screenshot shows the numbers
array unfolded under the Locals
scope.
It's also possible to change the value of a variable at runtime using the VARIABLES section. You can double-click the variable name and then enter a new value.
WATCH section
What if you want to track a variable state across time or different methods? It can be tedious to search for the variable every time. That's where the WATCH section comes in handy.
You can select the Add Expression button (appears as a plus sign: +) to
enter a variable name or an expression to watch. As an alternative, you can
right-click a variable in the VARIABLES section and select Add to watch
.
All expressions inside the WATCH section will be updated automatically as your code runs.
CALL STACK section
Every time your code enters a method from another method, a call layer is added to the application's call stack. When your application becomes complex and you have a long list of methods called by other methods, the call stack represents the trail of method calls.
The CALL STACK section is useful when you're trying to find the source location for an exception or WATCH expression. If your application throws an unexpected exception, you'll often see a message in the console that resembles the following:
Exception has occurred: CLR/System.DivideByZeroException
An unhandled exception of type 'System.DivideByZeroException' occurred in Debug1.dll: 'Attempted to divide by zero.'
at Program.<<Main>$>g__WriteMessage|0_1() in C:\Users\howdc\Desktop\Debug1\Program.cs:line 27
at Program.<<Main>$>g__Process1|0_0() in C:\Users\howdc\Desktop\Debug1\Program.cs:line 16
at Program.<Main>$(String[] args) in C:\Users\howdc\Desktop\Debug1\Program.cs:line 10
The indented group of at Program ...
lines under the error message is called
a stack trace. The stack trace lists the name and origin of every method that
was called leading up to the exception. The information can be a bit difficult
to decipher though, because it can also include information from the .NET
runtime. In this example, the stack trace is pretty clean and you can see that
exception occurred in a method named WriteMessage
. The stack originates in a
method named Main
, which is the top-level statements section of the console
application.
The CALL STACK section can help you to avoid the difficulty of deciphering a stack trace that's cluttered with .NET runtime information. It filters out unwanted information to show you only the relevant methods from your own code by default. You can manually unwind the call stack to find out where the exception originated.
BREAKPOINTS section
The BREAKPOINTS section displays the current breakpoint settings and can be used to enable or disable specific breakpoints during a debug session.
Configure your application and launch configuration
When you're working on a console application that reads user input, you'll probably need to update launch configuration file.
Update the code in your Program.cs file as follows:
string? readResult;
int startIndex = 0;
bool goodEntry = false;
int[] numbers = { 1, 2, 3, 4, 5 };
// Display the array to the console.
Console.Clear();
Console.Write("\n\rThe 'numbers' array contains: { ");
foreach (int number in numbers) {
Console.Write($"{number} ");
}
// To calculate a sum of array elements,
// prompt the user for the starting element number.
Console.WriteLine($"}}\n\r\n\rTo sum values 'n' through 5, enter a value for 'n':");
while (goodEntry == false) {
readResult = Console.ReadLine();
goodEntry = int.TryParse(readResult, out startIndex);
if (startIndex > 5) {
goodEntry = false;
Console.WriteLine("\n\rEnter an integer value between 1 and 5");
}
}
// Display the sum and then pause.
Console.WriteLine(
$"\n\rThe sum of numbers {startIndex} through " +
$"{numbers.Length} is: {SumValues(numbers, startIndex)}"
);
Console.WriteLine("press Enter to exit");
readResult = Console.ReadLine();
// This method returns the sum of elements n through 5
static int SumValues(int[] numbers, int n) {
int sum = 0;
for (int i = n; i < numbers.Length; i++) {
sum += numbers[i];
}
return sum;
}
Take a minute to review the code.
Notice the following:
- The code specifies an integer array containing five numbers.
- The code displays output in the console.
- The code prompts the user to enter a starting element number
n
that it uses to sum array elementsn
through5
. - The code calculates the sum in a method, displays the results in the console, and then pauses.
Note
The DEBUG CONSOLE panel does not support user input from the console.
On the Visual Studio Code File menu, select Save.
On the Run menu, select Remove All Breakpoints.
This removes any breakpoints left over from the previous exercise.
On the RUN AND DEBUG view, select Start Debugging.
Notice that an error occurs when the Console.Clear();
code line is executed.
On the Debug toolbar, select Stop.
Switch to the EXPLORER view, and then open the launch.json file in the Editor.
Update the value of the console
attribute as follows:
"console":"integratedTerminal",
On the Visual Studio Code File menu, select Save, and then close the launch.json file.
Review application output and identify issues
Reviewing the output of your application can reveal logic issues that you've overlooked when writing your code.
Switch back to the RUN AND DEBUG view.
On the RUN AND DEBUG view, select Start Debugging.
The messages displayed to the DEBUG CONSOLE panel show the debugger attaching
to the Debug101.dll
application.
Notice that no error messages are displayed.
Changing the value of the console
attribute from internalConsole to
integratedTerminal in the launch configuration file has fixed the console error. But now you need to locate the console that contains your output.
In the Panels area below the Editor, switch from the DEBUG CONSOLE panel to the TERMINAL panel.
Notice that code execution has paused at the message prompting the user to
enter a value for n
.
The output on the TERMINAL panel should look like the following:
The 'numbers' array contains: { 1 2 3 4 5 }
To sum values 'n' through 5, enter a value for 'n':
At the TERMINAL command prompt, enter 3
Review the output from the application.
The output on the TERMINAL panel should look like the following:
The 'numbers' array contains: { 1 2 3 4 5 }
To sum values 'n' through 5, enter a value for 'n':
3
The sum of numbers 3 through 5 is: 9
press Enter to exit
Take a minute to consider the reported value of sum
and the values of array
elements 3 through 5 displayed at the top of the console.
The message says: The sum of numbers 3 through 5 is: 9
. However, array
elements 3 through 5 are 3
, 4
, and 5
. Shouldn't the reported sum be 12?
You can use the VARIABLES section of the RUN AND DEBUG view to investigate the issue.
Monitor variable state
In some cases, simply monitoring variable state is enough to identify the logic issue in your application.
Set a breakpoint on the following code line:
Console.WriteLine($"\n\rThe sum of numbers {startIndex} through {numbers.Length} is: {SumValues(numbers, startIndex)}");
On the RUN AND DEBUG view, select Start Debugging.
Switch from the DEBUG CONSOLE panel to the TERMINAL panel.
At the TERMINAL command prompt, enter 3
Code execution will pause at the breakpoint.
Take a minute to review the VARIABLES section of the RUN AND DEBUG view.
Notice that startIndex
has been assigned the value that you entered, which is
3
.
Select Step Into.
Notice that the VARIABLES and CALL STACK sections are updated.
The CALL STACK section shows that code execution has moved into the SumValues
method.
The VARIABLES section, which lists the local variables, shows the value of the
integer n
. The method parameter n
is assigned its value from the method
call argument startIndex
. In this case, the change to variable names makes it
clear the value has been passed, not a reference pointer.
Note
In this case, you can see most of your code in the Editor, so you might not need the CALL STACK section, but when you're working on larger applications with deeply nested and interconnected method calls, the execution path shown in the CALL STACK section can be extremely useful.
Continue selecting Step Into until the value assigned to sum
is no longer 0
.
Take a minute to review the information shown in the VARIABLES section.
You should see the following: