Debugging .net core in VS Code

Debugging .net core in VS Code with Hot Reload (kind of)

I am a user of Visual Studio (VS) and Visual Studio Code (VSCode). I have found that debugging an application in VS is simpler to set up. However I like working in VSCode and would also like a similar setup there. Moreover, I also program in JavaScript environments and really like the idea of “hot reload”, meaning that I can change something in the code and keep on working without having to build, restart and attach the debugger manually. This will not work (or I don’t know how) with dotnet core and VSCode, but it is possible to do something similar. As an example I am using a aspnet core application called “SuperAPI” to debug. The project will be stored in <root>/SuperAPI.csproj and the sources for the project will be under <root>/Source/.

We will create a debug configuration that will start a watcher on your code directory and attach the debugger to the started process. When you change the code, the solution will be rebuilt (and the debugger detached). After the process is restarted the debugger will reattach, so you can have a workflow somewhat similar to live reload (except that the “live” part takes quite a long time, at least with my current setup).

This is an overview of what we want to do:

launches
launches
Start Debugging
Start Debugging
watch
watch
Restart Omnisharp
Restart Omnisharp
Restart Debugger
Restart Debugger
On Disconnect
(because process died)
On Disconnect...
Attaches to process
Attaches to process
Debugger
Debugger
Restarts Process on code changes
Restarts Process on code changes
Viewer does not support full SVG 1.1

There are two ingredients to get this working: launch.json and tasks.json, both located in the .vscode folder at the root of your project.

Tasks in VSCode can be started by Ctrl+Shift+P and typing Tasks: Run Task. I also like to use the Task Explorer Plugin

VSCode uses tasks.json to define what to run when you start that task. The first task we define is the watch task. It looks like this:

  {
            "label": "watch",
            "command": "dotnet",
            "type": "shell",
            "options": {
                "cwd": "${workspaceFolder}/Source/SuperAPI"
            },
            "args": [
                "watch",
                "run",
                "${workspaceFolder}/SuperAPI.csproj",
                "/property:GenerateFullPaths=true",
                "/consoleloggerparameters:NoSummary"
            ],
            "isBackground": true,
            "problemMatcher": {
                "owner": "custom",
                "pattern": {
                    "regexp": "_____"
                },
                "background": {
                    "activeOnStart": true,
                    "beginsPattern": "^.*$",
                    "endsPattern": "^.*Application started.*$"
                },
            },

It is the equivalent of running dotnet run watch <your csproj> /property:GenerateFullPaths=true /consoleloggerparameters:NoSummary in a separate console (in fact, if you prefer to run it in a separate window or the built-in terminal, you can do that instead of calling it through VSCode, because the later tasks will not be calling this again). The problemMatcher part is like saying “look at the output and wait until you get a line with ‘Application started’ in it. (BTW, if you don’t know regex’s, you should go an look them up now.)

Now the application is running, we need a debug configuration. Debug configurations are defined in launch.json and are the things that show up here:

debug

{
  "name": "Start Debugging",
  "type": "coreclr",
  "request": "attach",
  "preLaunchTask": "watch",
  "processName": "SuperAPI.exe",
  "postDebugTask": "RestartDebugger"
}

You see that there is a preLaunchTask (which you can remove if you rather run the command separately) and a postDebugTask. The postDebugTask is what is called after the debugger is terminated (which is what will happen once the process being debugged is killed as part of the watch functionality from dotnet run). The processName property defines how the process is found that should be debugged. I haven’t quite found out what is actually done (if it regexes the processes or something) but you probably know how to figure out the name of the exe your running (typically its <csproj-name>.exe)

The RestartDebugger task is again defined in tasks.json:

({
  "label": "RestartDebugger",
  "dependsOn": "RestartOmnisharp",
  "command": "${command:workbench.action.debug.run}"
},
{
  "label": "RestartOmnisharp",
  "command": "${command:o.restart}"
})

Notice that there is also a RestartOmnisharp task, which is executed before RestartDebugger. This is because for reasons unknown to me, Omnisharp (the thing that does all the magic in VSCode when you’re using .net) seems to grok when watch rebuilds the solution.

A word of warning: While this setup does work for me, it’s not perfect. The rebuild time after a change is really long and the reattachment of the debugger without visual feedback for a long time, which sometimes gives the impression that something has really gone wrong (which sometimes also happens, and the you’ll need to start from the beginning, maybe even having to restart VSCode)

As a reference here are the full tasks.json and launch.json:

launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Start Debugging",
      "type": "coreclr",
      "request": "attach",
      "preLaunchTask": "watch",
      "processName": "SuperAPI.exe",
      "postDebugTask": "RestartDebugger"
    }
  ]
}

tasks.json:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "watch",
      "command": "dotnet",
      "type": "shell",
      "options": {
        "cwd": "${workspaceFolder}/Source/SuperAPI"
      },
      "args": [
        "watch",
        "run",
        "${workspaceFolder}/SuperAPI.csproj",
        "/property:GenerateFullPaths=true",
        "/consoleloggerparameters:NoSummary"
      ],
      "isBackground": true,
      "problemMatcher": {
        "owner": "custom",
        "pattern": {
          "regexp": "_____"
        },
        "background": {
          "activeOnStart": true,
          "beginsPattern": "^.*$",
          "endsPattern": "^.*Application started.*$"
        }
      }
    },
    {
      "label": "RestartDebugger",
      "dependsOn": "RestartOmnisharp",
      "command": "${command:workbench.action.debug.run}"
    },
    {
      "label": "RestartOmnisharp",
      "command": "${command:o.restart}"
    }
  ]
}