You know how to set breakpoints and step through code. This guide covers the non-obvious debugging workflows for VSCode and .NET: debugging inside containers with hot reload, multi-solution monorepos, Dev Tunnels for webhook testing, cross-platform symbol loading issues, and C# Dev Kit quirks in enterprise environments.
Prerequisites: C# Dev Kit or C# extension installed, .NET SDK, basic familiarity with launch.json and tasks.json. For basics, see official VSCode .NET debugging docs.
Table of Contents
- Real-World Debugging Workflows
- Multi-Root Workspaces & Security
- Troubleshooting C# Dev Kit Issues
- Performance Optimization
- Advanced Configuration Patterns
- Lesser-Known Features
Multi-Root Workspaces & Security
Sharing Configurations Across Microservices
When working with microservices or monorepos, you can define debug configurations in a .code-workspace file instead of individual .vscode/launch.json files. This centralizes all your debug configurations in one place.
A workspace file includes a launch section where configurations reference specific folders using ${workspaceFolder:folderName}. You can also define compounds to launch multiple debug sessions simultaneously—perfect for debugging an API alongside a background worker.
{
"folders": [
{ "path": "services/api" },
{ "path": "services/worker" }
],
"launch": {
"configurations": [
{
"name": "Debug API",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder:api}/bin/Debug/net10.0/API.dll",
"cwd": "${workspaceFolder:api}"
}
],
"compounds": [
{
"name": "Debug All Services",
"configurations": ["Debug API", "Debug Worker"]
}
]
}
}
Keeping Secrets Out of launch.json
Never commit sensitive data to launch.json. Here are secure alternatives:
Option 1: Environment Variables
Reference environment variables using ${env:VARIABLE_NAME}:
{
"env": {
"ConnectionString": "${env:DB_CONNECTION_STRING}",
"ApiKey": "${env:API_KEY}"
}
}
Set these in your shell before launching VSCode, or use a .env file with the envFile property (ensure .env is git-ignored).
Option 2: .NET User Secrets
For .NET projects, use the built-in User Secrets feature. Just set the environment to Development and your app will automatically load secrets from the user secrets store (~/.microsoft/usersecrets/):
{
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
Option 3: Interactive Inputs
For values that change frequently (like selecting which database environment), use inputs to prompt during launch:
{
"configurations": [
{
"env": {
"DATABASE": "${input:databaseSelection}"
}
}
],
"inputs": [
{
"id": "databaseSelection",
"type": "pickString",
"description": "Select database environment",
"options": ["local", "dev", "staging"],
"default": "local"
}
]
}
Understanding launch.json Structure
Here’s a detailed breakdown:
{
"version": "0.2.0",
"configurations": [
{
// Human-readable name shown in debug dropdown
"name": ".NET Core Launch (console)",
// Debugger type: "coreclr" for .NET Core/5+
"type": "coreclr",
// "launch" = start new process
// "attach" = attach to running process
"request": "launch",
// Task to run before debugging
"preLaunchTask": "build",
// Path to compiled .dll
"program": "${workspaceFolder}/bin/Debug/net10.0/YourApp.dll",
// Command line arguments
"args": [],
// Working directory when app runs
"cwd": "${workspaceFolder}",
// Console type: "integratedTerminal", "internalConsole", "externalTerminal"
"console": "integratedTerminal",
// Whether to break at entry point
"stopAtEntry": false
}
]
}
Variable Substitution
VSCode supports these variables in launch.json:
${workspaceFolder}- Root folder opened in VSCode${workspaceFolderBasename}- Folder name without slashes${file}- Currently opened file${fileBasename}- Currently opened filename${fileDirname}- Directory of currently opened file${fileExtname}- Extension of current file${cwd}- Current working directory${env:VARIABLE}- Environment variable
Real-World Example: Multiple Launch Configurations
Here’s a practical example showing multiple configurations for a CLI application:
{
"version": "0.2.0",
"configurations": [
{
"name": "CLI: Import Data",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net10.0/MyCliApp.dll",
"args": ["import", "--source", "data.csv", "--batch-size", "100"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"stopAtEntry": false,
"env": {
"DATABASE_CONNECTION": "Server=localhost;Database=mydb;",
"LOG_LEVEL": "Debug"
}
},
{
"name": "CLI: Export Data",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net10.0/MyCliApp.dll",
"args": ["export", "--format", "json", "--output", "export.json"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"stopAtEntry": false,
"env": {
"DATABASE_CONNECTION": "Server=localhost;Database=mydb;"
}
},
{
"name": "CLI: Process Queue",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net10.0/MyCliApp.dll",
"args": ["process", "--workers", "5", "--timeout", "30"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"stopAtEntry": false,
"env": {
"QUEUE_CONNECTION": "Endpoint=sb://...",
"ENVIRONMENT": "Development"
}
},
{
"name": "CLI: Custom Arguments",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net10.0/MyCliApp.dll",
"args": [],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"stopAtEntry": false,
"env": {
"DATABASE_CONNECTION": "Server=localhost;Database=mydb;"
}
}
]
}
Key Configuration Properties Explained
1. Console Types
"console": "integratedTerminal" // VSCode's integrated terminal (recommended)
"console": "internalConsole" // Debug Console panel (no input support)
"console": "externalTerminal" // System terminal window
When to use each:
- integratedTerminal: Best for most scenarios, allows user input
- internalConsole: Read-only output, good for simple logging
- externalTerminal: When you need native terminal features
2. Environment Variables
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"SERVICEBUS_CONNECTION_STRING": "Endpoint=sb://...",
"LOG_LEVEL": "Debug"
}
3. Source File Map (for debugging published apps)
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
4. Just My Code
"justMyCode": false // Enable to step into framework code
5. Require Exact Source
"requireExactSource": false // Allow debugging when source doesn't match
Configuration for Different Project Types
Console Application
{
"name": ".NET Console App",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net10.0/ConsoleApp.dll",
"args": [],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"stopAtEntry": false
}
ASP.NET Core Web API
{
"name": ".NET Web API",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/net10.0/WebApi.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:5001;http://localhost:5000"
},
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
}
}
The serverReadyAction automatically opens your browser when the web server is ready.
Blazor WebAssembly
{
"name": "Blazor WASM",
"type": "blazorwasm",
"request": "launch",
"cwd": "${workspaceFolder}",
"browser": "edge"
}
Attach to Running Process
{
"name": "Attach to Process",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
When you debug with this configuration, VSCode shows a process picker.
Configuring tasks.json
The tasks.json file defines automated tasks like building, testing, and publishing.
Auto-Generating tasks.json
- Press Ctrl+Shift+P (Cmd+Shift+P on macOS)
- Type “Tasks: Configure Task”
- Select “Create tasks.json from template”
- Choose ”.NET Core”
Understanding tasks.json Structure
{
"version": "2.0.0",
"tasks": [
{
// Human-readable task name
"label": "build",
// Command to execute
"command": "dotnet",
// Process type (vs "shell")
"type": "process",
// Command arguments
"args": [
"build",
"${workspaceFolder}/YourProject.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
// How to detect errors in output
"problemMatcher": "$msCompile",
// Task grouping
"group": {
"kind": "build",
"isDefault": true // This is the default build task
}
}
]
}
Complete tasks.json Example
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/YourProject.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/YourProject.csproj",
"-c", "Release",
"-o", "${workspaceFolder}/publish",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/YourProject.csproj"
],
"problemMatcher": "$msCompile",
"isBackground": true
},
{
"label": "clean",
"command": "dotnet",
"type": "process",
"args": [
"clean",
"${workspaceFolder}/YourProject.csproj"
],
"problemMatcher": "$msCompile"
},
{
"label": "test",
"command": "dotnet",
"type": "process",
"args": [
"test",
"${workspaceFolder}/YourProject.Tests.csproj"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "test",
"isDefault": true
}
}
]
}
Task Properties Explained
Problem Matchers
Problem matchers parse task output to detect errors and warnings:
$msCompile- MSBuild/C# compiler errors$tsc- TypeScript compiler$eslint-compact- ESLint errors
Custom problem matcher:
"problemMatcher": {
"owner": "custom",
"fileLocation": ["relative", "${workspaceFolder}"],
"pattern": {
"regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
}
Task Dependencies
Run multiple tasks in sequence:
{
"label": "build-and-test",
"dependsOn": ["clean", "build", "test"],
"dependsOrder": "sequence"
}
Background Tasks
For long-running processes like dotnet watch:
{
"label": "watch",
"isBackground": true,
"problemMatcher": {
"base": "$msCompile",
"background": {
"activeOnStart": true,
"beginsPattern": "^\\s*Waiting for a file to change",
"endsPattern": "^\\s*Application started"
}
}
}
Real-World Debugging Workflows
Debugging Containerized ASP.NET with Hot Reload
When developing containerized applications, you can debug inside the container while maintaining hot reload capabilities.
docker-compose.debug.yml:
services:
api:
build:
context: .
dockerfile: Dockerfile.debug
ports:
- "5000:5000"
- "5001:5001"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- DOTNET_USE_POLLING_FILE_WATCHER=true
volumes:
- ./src:/app/src:ro
- ./bin/Debug:/app/bin/Debug
launch.json for container debugging:
{
"name": "Docker .NET Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickRemoteProcess}",
"pipeTransport": {
"pipeProgram": "docker",
"pipeArgs": ["exec", "-i", "myapi-container"],
"debuggerPath": "/vsdbg/vsdbg",
"pipeCwd": "${workspaceFolder}"
},
"sourceFileMap": {
"/app": "${workspaceFolder}"
}
}
Workflow:
- Start container with
docker-compose -f docker-compose.debug.yml up - Container includes vsdbg debugger
- Use “Docker .NET Attach” configuration to attach
- Hot reload works via volume mounts
- Set breakpoints and debug as if running locally
Common gotcha: Ensure your Dockerfile.debug installs vsdbg:
RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg
Data Breakpoints: Platform-Specific Gotchas
Data breakpoints (break on value change) have platform-specific limitations you should know about.
What works everywhere:
- Simple value types (int, bool, string)
- Fields in classes (not properties with getters/setters)
- Local variables in the current stack frame
Linux/macOS limitations:
// ✅ Works on all platforms
private int _counter;
public void Increment() {
_counter++; // Data breakpoint here works
}
// ❌ Doesn't work reliably on Linux
public int Counter { get; set; } // Auto-property
private List<int> _items; // Collection internals
// ✅ Workaround: Break on backing field
private int _counterBackingField;
public int Counter {
get => _counterBackingField;
set {
_counterBackingField = value; // Set data breakpoint here
}
}
Performance impact: Data breakpoints slow execution significantly because every memory write is checked. Use sparingly in tight loops.
Best use case: Tracking down state corruption bugs where you don’t know which code path modifies a field.
Debugging with Dev Tunnels
Dev Tunnels let you debug locally while exposing your app publicly—perfect for testing webhooks, OAuth callbacks, or mobile apps hitting your API.
Setup:
# Install devtunnel CLI
dotnet tool install -g Microsoft.devtunnels.cli
# Create persistent tunnel
devtunnel create --allow-anonymous
devtunnel port create -p 5000
# Start tunnel
devtunnel host
launch.json with Dev Tunnel:
{
"name": "ASP.NET with Dev Tunnel",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/net10.0/MyApi.dll",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:5000",
"PUBLIC_URL": "https://abc123.devtunnels.ms"
},
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+http://localhost:5000",
"uriFormat": "https://abc123.devtunnels.ms"
}
}
Real-world scenario: Debugging Stripe webhooks
- Configure Stripe webhook URL:
https://abc123.devtunnels.ms/api/webhooks/stripe - Start debugging with tunnel configuration
- Trigger webhook from Stripe dashboard
- Breakpoint hits in your local code
- Inspect webhook payload and debug business logic
Dev Tunnels with --allow-anonymous are publicly accessible. Never expose production credentials or sensitive data.
Debugging Hot Reload Edge Cases
Hot Reload is powerful but has surprising limitations. Here’s what works and what doesn’t:
✅ What you CAN hot reload:
- Method body changes
- Adding new methods to existing classes
- Lambda expressions and LINQ queries
- String literals and constants
- Adding/removing
usingstatements
❌ What requires restart:
- Adding/removing class members (fields, properties)
- Changing method signatures
- Modifying generic type parameters
- Changes to
async/awaitboundaries - Attribute additions/removals
- Changes to records or primary constructors
Advanced technique - Partial classes for hot reload:
// ProductService.cs (stable structure)
public partial class ProductService {
private readonly ILogger _logger;
private readonly DbContext _db;
public ProductService(ILogger logger, DbContext db) {
_logger = logger;
_db = db;
}
}
// ProductService.Logic.cs (frequently changed)
public partial class ProductService {
public async Task<Product> GetProduct(int id) {
// Method body changes here support hot reload
// Even if you add new private methods to this partial
return await _db.Products.FindAsync(id);
}
}
Workflow tip: If hot reload fails, VSCode shows ”🔥 Hot Reload failed” in status bar. Click it to see what blocked the reload.
Cross-Platform Debugging Differences
Debugging behavior varies across Windows, Linux, and macOS. Here are the key differences:
Debugger attachment speed:
- Windows: ~500ms (native debugging APIs)
- Linux: ~1-2s (ptrace overhead)
- macOS: ~2-3s (System Integrity Protection checks)
Process enumeration (pickProcess):
// Works everywhere but shows different info
"processId": "${command:pickProcess}"
// Windows: Shows process name + PID + user
// Linux: Shows only process name + PID
// macOS: May require Full Disk Access permission
Symbol loading:
- Windows: Loads PDB files automatically from build output
- Linux/macOS: Uses portable PDB format (ensure
<DebugType>portable</DebugType>)
File path casing:
// Case-sensitive on Linux/macOS, insensitive on Windows
"sourceFileMap": {
"/app/src": "${workspaceFolder}/src" // Must match case on Linux!
}
Permission issues on Linux:
# If attach fails with "Operation not permitted"
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
# Or run VSCode with ptrace capability
sudo setcap cap_sys_ptrace=eip /usr/share/code/code
Debugging Memory Leaks with Diagnostic Tools
VSCode can trigger .NET diagnostic tools during debugging sessions.
Capture memory dump at breakpoint:
{
"name": "Debug with Memory Dump",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/net10.0/MyApp.dll",
"stopAtEntry": false,
"console": "integratedTerminal",
"preLaunchTask": "build",
"postDebugTask": "collect-memory-dump"
}
tasks.json:
{
"label": "collect-memory-dump",
"type": "shell",
"command": "dotnet-dump",
"args": ["collect", "--process-id", "${command:pickProcess}", "--output", "dump.dmp"]
}
Analyze in Debug Console during session:
// Check GC stats
> System.GC.GetTotalMemory(false)
> System.GC.CollectionCount(0)
// Inspect object counts (requires ObjectInspector NuGet)
> ObjectInspector.GetObjectCount(typeof(MyHeavyObject))
Real scenario: Finding a HttpClient leak
- Set breakpoint after suspected leak point
- Use Debug Console:
System.GC.GetTotalMemory(false) - Continue, trigger leak scenario, hit breakpoint again
- Memory increased significantly
- Collect dump:
dotnet-dump collect - Analyze:
dotnet-dump analyze dump.dmp - In dump:
dumpheap -type HttpClientshows undisposed instances
Advanced Debugging Techniques
1. Data Breakpoints (Memory Breakpoints)
Break when a variable’s value changes:
In Variables view:
- Right-click variable
- Select “Break on Value Change”
2. Exception Settings
Configure which exceptions break execution:
- Open Run and Debug view
- Click “Exception Settings” in toolbar
- Check/uncheck exception types
Or in launch.json:
"exceptionOptions": {
"breakOnAll": false,
"breakOnAny": [
"System.NullReferenceException",
"System.ArgumentException"
]
}
3. Edit and Continue
Modify code while debugging (limited support in .NET):
- Pause execution at breakpoint
- Edit code
- Continue - changes apply if possible
Enable in launch.json:
"enableStepFiltering": false,
"enableEditAndContinue": true
4. Source Link
Debug NuGet packages with original source code:
"justMyCode": false,
"requireExactSource": false,
"symbolOptions": {
"searchPaths": [],
"searchMicrosoftSymbolServer": true,
"searchNuGetOrgSymbolServer": true
}
5. Multi-Target Debugging
Debug multiple processes simultaneously:
{
"version": "0.2.0",
"configurations": [
{
"name": "API",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/api/bin/Debug/net10.0/api.dll"
},
{
"name": "Worker",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/worker/bin/Debug/net10.0/worker.dll"
}
],
"compounds": [
{
"name": "API + Worker",
"configurations": ["API", "Worker"],
"stopAll": true
}
]
}
6. Remote Debugging
Debug application running on another machine:
On remote machine:
# Install vsdbg
curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg
launch.json:
{
"name": "Remote Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickRemoteProcess}",
"pipeTransport": {
"pipeCwd": "${workspaceFolder}",
"pipeProgram": "ssh",
"pipeArgs": [
"user@remote-host"
],
"debuggerPath": "~/vsdbg/vsdbg"
}
}
7. Hot Reload
See code changes without restarting:
Start with:
dotnet watch
Or in tasks.json:
{
"label": "watch",
"command": "dotnet",
"args": ["watch", "--project", "${workspaceFolder}/YourProject.csproj"],
"isBackground": true
}
8. Debugging Tests
Create test-specific configuration:
{
"name": "Debug Tests",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"test",
"${workspaceFolder}/tests/YourProject.Tests/YourProject.Tests.csproj",
"--filter", "FullyQualifiedName~YourNamespace.YourTestClass.YourTestMethod"
],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal"
}
Or use Test Explorer:
- Open Testing view (beaker icon in sidebar)
- Right-click test
- Select “Debug Test”
9. Logpoints (Non-Breaking Breakpoints)
Log without stopping execution:
- Right-click line number
- Select “Add Logpoint”
- Enter message:
Message count: {messages.Count}
10. Function Breakpoints
Break when specific function is called:
- Debug view > Breakpoints section
- Click + dropdown
- Select “Function Breakpoint”
- Enter function name:
MyNamespace.MyClass.MyMethod
Workspace Settings for .NET
Configure .vscode/settings.json for better .NET experience:
{
// C# specific
"dotnet.defaultSolution": "YourSolution.sln",
"csharp.semanticHighlighting.enabled": true,
"csharp.suppressDotnetInstallWarning": false,
// IntelliCode
"csharp.inlayHints.enableInlayHintsForParameters": true,
"csharp.inlayHints.enableInlayHintsForLiteralParameters": true,
"csharp.inlayHints.enableInlayHintsForIndexerParameters": true,
"csharp.inlayHints.enableInlayHintsForObjectCreationParameters": true,
"csharp.inlayHints.enableInlayHintsForOtherParameters": true,
"csharp.inlayHints.suppressInlayHintsForParametersThatDifferOnlyBySuffix": false,
// Code formatting
"omnisharp.enableEditorConfigSupport": true,
"omnisharp.enableRoslynAnalyzers": true,
"omnisharp.organizeImportsOnFormat": true,
// Terminal
"terminal.integrated.env.windows": {
"PATH": "${env:PATH};C:\\Program Files\\dotnet"
},
// Files
"files.exclude": {
"**/bin": true,
"**/obj": true
},
// Search
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/bin": true,
"**/obj": true
}
}
Troubleshooting C# Dev Kit & VSCode-Specific Issues
Symbol Loading Behind Corporate Proxies
Problem: Debugger can’t download symbols from symbol servers even with searchMicrosoftSymbolServer: true.
Root cause: C# Dev Kit doesn’t inherit system proxy settings automatically.
Solution:
# Set environment variables before launching VSCode
export HTTP_PROXY=http://proxy.company.com:8080
export HTTPS_PROXY=http://proxy.company.com:8080
export NO_PROXY=localhost,127.0.0.1,.company.internal
code .
Or configure in settings.json:
{
"http.proxy": "http://proxy.company.com:8080",
"http.proxyStrictSSL": false // Only if corporate proxy uses self-signed certs
}
Verify symbols download:
# Check symbol cache location
ls ~/.dotnet/symbolcache/
# Force re-download
rm -rf ~/.dotnet/symbolcache/
Multi-Solution Repos with Shared Projects
Problem: C# Dev Kit loads ALL .sln files in the workspace, causing massive IntelliSense overhead and conflicting project references.
Scenario: Monorepo with Services.sln, Tests.sln, Tools.sln all referencing Shared.csproj.
Solution 1: Explicit default solution
{
"dotnet.defaultSolution": "Services.sln"
}
Solution 2: Disable auto-discovery
{
"dotnet.enableSolutionDiscovery": false, // Prevents scanning for .sln files
"dotnet.defaultSolution": "${workspaceFolder}/src/Main.slnf"
}
Generate a solution filter (.slnf) programmatically:
# List all projects in solution
dotnet sln Services.sln list
# Create filter with only needed projects
cat > Debug.slnf <<EOF
{
"solution": {
"path": "Services.sln",
"projects": [
"src/API/API.csproj",
"src/Shared/Shared.csproj"
]
}
}
EOF
Debugger Attaches But Symbols Don’t Load
Problem: “The breakpoint will not currently be hit. No symbols have been loaded for this document.”
This happens when:
- Source file path mismatch (common in CI-built binaries)
- PDB files are in different location than DLL
- Portable vs Windows PDB format mismatch
Diagnosis:
# Check PDB location embedded in DLL
dotnet-pdb dump MyApp.dll
# Verify PDB format
file MyApp.pdb
# Should output: "Portable pdb" for cross-platform debugging
Fix for source path mismatch:
{
"sourceFileMap": {
"/build/src": "${workspaceFolder}/src", // CI build paths
"C:\\Jenkins\\workspace": "${workspaceFolder}" // Windows CI
},
"requireExactSource": false // Allow debugging even if source differs slightly
}
Fix for PDB location:
{
"symbolOptions": {
"searchPaths": [
"${workspaceFolder}/bin/Debug/net10.0",
"${workspaceFolder}/lib" // If PDBs are in separate directory
],
"searchMicrosoftSymbolServer": true,
"cachePath": "${workspaceFolder}/.symbols" // Local symbol cache
}
}
C# Dev Kit Language Server Crashes in Large Repos
Symptoms: “The C# server has encountered a fatal error” or constant restarts.
Check logs:
# View C# extension logs
code --log-level trace
# Or from Command Palette
# "Developer: Show Logs" > "C# Dev Kit"
Common causes:
- Roslyn analyzer overload (e.g., running StyleCop on 100+ projects)
- Too many projects loaded simultaneously
- Circular project references
Solutions:
{
// Disable analyzers during debugging sessions
"omnisharp.enableRoslynAnalyzers": false,
// Limit analysis scope
"dotnet.backgroundAnalysis.analyzerDiagnosticsScope": "openFiles",
// Increase language server memory
"omnisharp.maxProjectFileCountForDiagnosticAnalysis": 50, // Default: unlimited
// Disable features you don't use
"dotnet.codeLens.enableReferencesCodeLens": false,
"csharp.inlayHints.enableInlayHintsForParameters": false
}
Nuclear option - restart language server:
Ctrl+Shift+P > "C#: Restart Server"
Debugging Doesn’t Work After Upgrading .NET SDK
Problem: After dotnet upgrade, debugger fails with “Unknown debugger type: coreclr”.
Cause: C# extension caches old SDK location.
Fix:
# 1. Uninstall all .NET SDKs
dotnet --list-sdks
# Note the paths, then uninstall
# 2. Clean VSCode extension cache
rm -rf ~/.vscode/extensions/ms-dotnettools.*
# 3. Reinstall latest SDK
# Download from https://dotnet.microsoft.com/download
# 4. Reinstall C# Dev Kit extension
code --install-extension ms-dotnettools.csdevkit
# 5. Reload VSCode
Verify:
dotnet --version
# Should match your expected SDK
# In VSCode, check OmniSharp is using correct SDK
# Output panel > "C#" > Look for "Using dotnet CLI from: ..."
Breakpoints in Razor/Blazor Files Not Hitting
Problem: Breakpoints in .razor files are ignored or show as “unverified”.
Cause: Blazor debugging requires browser debugging, not just CoreCLR debugger.
launch.json for Blazor Server:
{
"name": "Blazor Server",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/net10.0/MyApp.dll",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"serverReadyAction": {
"action": "debugWithChrome", // Not "openExternally"!
"pattern": "\\bNow listening on:\\s+(https?://\\S+)",
"uriFormat": "%s"
}
}
For Blazor WebAssembly:
{
"name": "Blazor WASM",
"type": "blazorwasm", // Different debugger type
"request": "launch",
"cwd": "${workspaceFolder}/Client",
"browser": "edge", // or "chrome"
"trace": true // Enable for debugging debugger issues
}
Razor breakpoints require:
- Line must contain C# code (not HTML/markup)
- Use
@codeblock or inline@{ }expressions - Hot reload must be disabled for breakpoint to bind
Multi-Targeting Projects Break Debugging
Problem: Project targets <TargetFrameworks>net8.0;net10.0</TargetFrameworks>, debugger picks wrong framework.
Solution: Specify exact framework in launch.json:
{
"program": "${workspaceFolder}/bin/Debug/net10.0/MyApp.dll", // Explicit net10.0
"env": {
"DOTNET_TARGET_FRAMEWORK": "net10.0" // Force specific TFM
}
}
Or build for single framework:
dotnet build -f net10.0
tasks.json to ensure correct framework:
{
"label": "build-net10",
"command": "dotnet",
"args": [
"build",
"${workspaceFolder}/MyApp.csproj",
"-f", "net10.0",
"-c", "Debug"
],
"group": {
"kind": "build",
"isDefault": true
}
}
Performance Optimization for Large Solutions
Large .NET solutions can slow down VSCode. Here’s how to optimize:
Selective Project Loading
Use solution filters (.slnf) to load only relevant projects:
{
"solution": {
"path": "FullSolution.sln",
"projects": [
"src/Core/Core.csproj",
"src/API/API.csproj",
"tests/Core.Tests/Core.Tests.csproj"
]
}
}
Point to it in settings:
{
"dotnet.defaultSolution": "Development.slnf"
}
Impact: Reduces IntelliSense indexing time from ~60s to ~10s in a 50-project solution.
Disable Heavy Features for Debugging-Only Workflows
{
// Stop background analysis for files you're not editing
"dotnet.backgroundAnalysis.analyzerDiagnosticsScope": "openFiles",
// Disable CodeLens references (expensive in large codebases)
"dotnet.codeLens.enableReferencesCodeLens": false,
// Reduce inlay hints noise
"csharp.inlayHints.enableInlayHintsForParameters": false,
// Skip Roslyn analyzers during debugging (run in CI instead)
"omnisharp.enableRoslynAnalyzers": false
}
Debug-Specific Workspace Settings
Create .vscode/settings.json for debug-focused work:
{
// Hide build output to reduce clutter
"files.exclude": {
"**/bin": true,
"**/obj": true,
"**/.vs": true
},
// Don't watch bin/obj for changes (saves CPU)
"files.watcherExclude": {
"**/bin/**": true,
"**/obj/**": true
},
// Faster file search
"search.exclude": {
"**/bin": true,
"**/obj": true,
"**/node_modules": true
}
}
OmniSharp Memory Limits
For monorepos, increase OmniSharp’s heap size:
{
"omnisharp.maxProjectResults": 50, // Default: 250
"omnisharp.dotNetCliOptions": ["-m", "4096"] // 4GB heap for OmniSharp
}
Advanced Configuration Patterns
Environment-Specific Configurations with Inheritance
Use a base configuration and override per environment:
{
"version": "0.2.0",
"configurations": [
{
"name": "Base Config",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/net10.0/MyApp.dll",
"cwd": "${workspaceFolder}",
"env": {
"LOG_LEVEL": "Information"
}
},
{
"name": "Development",
"preLaunchTask": "build",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DATABASE": "localhost"
}
},
{
"name": "Staging",
"preLaunchTask": "build",
"env": {
"ASPNETCORE_ENVIRONMENT": "Staging",
"DATABASE": "${env:STAGING_DB}",
"ENABLE_PROFILING": "true"
}
}
]
}
Conditional Launch Based on File Type
Launch different configurations based on which file is open:
{
"configurations": [
{
"name": "Debug Current Test",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"test",
"--filter",
"FullyQualifiedName~${fileBasenameNoExtension}"
],
"cwd": "${fileDirname}"
}
]
}
When you have a test file UserServiceTests.cs open and hit F5, it runs only tests from that file.
Debugging with Custom Entry Points
For testing specific scenarios without modifying Main():
{
"name": "Debug Specific Method",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/net10.0/MyApp.dll",
"args": ["--debug-entry", "MyNamespace.DebugEntryPoints.TestDataIngestion"],
"cwd": "${workspaceFolder}"
}
Program.cs:
if (args.Contains("--debug-entry"))
{
var entryPoint = args[Array.IndexOf(args, "--debug-entry") + 1];
var method = Type.GetType(entryPoint)?.GetMethod("Run");
method?.Invoke(null, null);
return;
}
Lesser-Known Debugging Features
Inline Value Display
Show variable values inline during debugging without hovering:
{
"debug.inlineValues": "on" // Shows values next to variable declarations
}
Breakpoint Hit Logging to File
Log every breakpoint hit without stopping execution:
Create a logpoint (right-click line > Add Logpoint):
{DateTime.Now:HH:mm:ss} - User {userId} - Order {orderId}
Messages appear in Debug Console. For file logging, combine with output redirection:
{
"console": "externalTerminal",
"args": ["2>&1", "|", "tee", "debug-log.txt"]
}
Source Map for Refactored Code
When debugging code that’s been moved/refactored:
{
"sourceFileMap": {
"C:\\OldPath\\": "${workspaceFolder}/NewPath/",
"/old/linux/path": "${workspaceFolder}/new-path"
}
}
Useful when debugging published/deployed code that differs from your current source tree.
Attach to Multiple Processes by Name
Instead of picking from a list, filter by name pattern:
{
"name": "Attach to MyService",
"type": "coreclr",
"request": "attach",
"processName": "MyService.exe" // Attaches to first match
}
For multiple instances, use a script:
# attach-all.sh
for pid in $(pgrep -f "MyService"); do
echo "Attaching to PID $pid"
# Trigger attach via VSCode command
done
Additional Resources
Official Documentation
Diagnostic Tools
- dotnet-dump - Memory dump capture and analysis
- dotnet-trace - Performance tracing
- dotnet-counters - Real-time performance monitoring
Community Resources
Conclusion
Setting up VSCode for .NET debugging is straightforward once you understand the key configuration files:
- launch.json - How to run and debug
- tasks.json - Build automation
- settings.json - Workspace preferences
Start with auto-generated configurations and customize as your project grows. Use multiple configurations for different scenarios, leverage environment variables for secrets, and explore advanced features like conditional breakpoints and hot reload.
With this setup, you’ll have a professional .NET development environment that rivals Visual Studio in debugging capabilities while maintaining VSCode’s speed and flexibility.