Advanced .NET Debugging in VSCode: Beyond the Basics

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


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

  1. Press Ctrl+Shift+P (Cmd+Shift+P on macOS)
  2. Type “Tasks: Configure Task”
  3. Select “Create tasks.json from template”
  4. 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:

  1. Start container with docker-compose -f docker-compose.debug.yml up
  2. Container includes vsdbg debugger
  3. Use “Docker .NET Attach” configuration to attach
  4. Hot reload works via volume mounts
  5. 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

  1. Configure Stripe webhook URL: https://abc123.devtunnels.ms/api/webhooks/stripe
  2. Start debugging with tunnel configuration
  3. Trigger webhook from Stripe dashboard
  4. Breakpoint hits in your local code
  5. Inspect webhook payload and debug business logic
Important

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 using statements

❌ What requires restart:

  • Adding/removing class members (fields, properties)
  • Changing method signatures
  • Modifying generic type parameters
  • Changes to async/await boundaries
  • 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

  1. Set breakpoint after suspected leak point
  2. Use Debug Console: System.GC.GetTotalMemory(false)
  3. Continue, trigger leak scenario, hit breakpoint again
  4. Memory increased significantly
  5. Collect dump: dotnet-dump collect
  6. Analyze: dotnet-dump analyze dump.dmp
  7. In dump: dumpheap -type HttpClient shows undisposed instances

Advanced Debugging Techniques

1. Data Breakpoints (Memory Breakpoints)

Break when a variable’s value changes:

In Variables view:

  1. Right-click variable
  2. Select “Break on Value Change”

2. Exception Settings

Configure which exceptions break execution:

  1. Open Run and Debug view
  2. Click “Exception Settings” in toolbar
  3. 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):

  1. Pause execution at breakpoint
  2. Edit code
  3. Continue - changes apply if possible

Enable in launch.json:

"enableStepFiltering": false,
"enableEditAndContinue": true

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:

  1. Open Testing view (beaker icon in sidebar)
  2. Right-click test
  3. Select “Debug Test”

9. Logpoints (Non-Breaking Breakpoints)

Log without stopping execution:

  1. Right-click line number
  2. Select “Add Logpoint”
  3. Enter message: Message count: {messages.Count}

10. Function Breakpoints

Break when specific function is called:

  1. Debug view > Breakpoints section
  2. Click + dropdown
  3. Select “Function Breakpoint”
  4. 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:

  1. Source file path mismatch (common in CI-built binaries)
  2. PDB files are in different location than DLL
  3. 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:

  1. Roslyn analyzer overload (e.g., running StyleCop on 100+ projects)
  2. Too many projects loaded simultaneously
  3. 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 @code block 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

Community Resources


Conclusion

Setting up VSCode for .NET debugging is straightforward once you understand the key configuration files:

  1. launch.json - How to run and debug
  2. tasks.json - Build automation
  3. 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.