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:
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:
{
"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}"
}
]
}