ms_learn_csharp/033_Debug_console_apps_2/033_csharp.md
2024-09-30 13:26:41 -03:00

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.

img

The Run menu provides menu options that are grouped into six sections.

  1. Start and stop applications. This section of the menu includes options for starting and stopping code execution, with and without the debugger attached.

  2. Launch configurations. This section of the menu provides access to examine or create launch configurations.

  3. 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.

  4. 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.

  5. Manage Breakpoints. This section of the menu enables the developer to manage breakpoints in bulk rather than individually.

  6. 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.

  1. Run and Debug controls panel. Used to configure and start a debug session.

  2. VARIABLES section. Used to view and manage variable state during a debug session.

  3. 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.

  4. 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).

  5. BREAKPOINTS section. Displays the current breakpoint settings.

  6. Debug toolbar. Used to control code execution during the debug process. This toolbar is only displayed while the application is running.

  7. 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).

  8. 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 related Console 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:

img

  1. Start debugging. This button (a green arrow) is used to start a debug session.

  2. Launch configurations. This dropdown menu provides access to launch configurations. The selected option is displayed.

  3. 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.

  4. 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.

img

  1. Pause/Continue. This button can be used to pause execution when the code is running and Continue when code execution has been paused.

  2. Step Over. This button can be used to execute the next method as a single command without inspecting or following its component steps.

  3. Step Into. This button can be used to enter the next method or code line and observe line-by-line execution steps.

  4. 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.

  5. Restart. This button can be used to terminate the current program execution and start debugging again using the current configuration.

  6. 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 setting debug.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.

img

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.

img

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.

img

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.

img

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.

img

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 is Sophia and the if statement is checking for the name Sophia, the expression evaluates to true and code execution moves into the code block of the if 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 the if 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 a Console.ReadLine() statement. When you're working on a C# console application that reads user input, the console setting must be set to either integratedTerminal or externalTerminal. 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.

img

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.

img

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:

img

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, and console.
  • 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.

img

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.

img

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";

img

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).

img

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.

img

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.

img

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 named Main.

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 elements n through 5.
  • 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: