Skip to content

[Debugger] DAP variables request returns empty array during ASP.NET Core HTTP request processing #8838

@odinGitGmail

Description

@odinGitGmail

Background & Motivation

I'm developing a VSCode extension to enhance the C# debugging experience. The original motivation was addressing a pain point: VSCode's Debug Console displays exception stack traces with file paths, but clicking them only navigates to line 1 of the file, not the actual error line. This forces developers to manually search for the error location.

My Extension's Features

To solve this, I created a custom debug window extension that provides:

  1. Enhanced Stack Trace Navigation

    • Clickable file paths that navigate directly to the specific line number (not just line 1)
    • Example: Clicking "Program.cs:line 6" opens Program.cs at line 6
  2. Smart Variable Parsing & Visualization

    • Automatic detection and rendering of DataTable, JSON, Array, List types
    • Table view with pagination and filtering capabilities
    • Better visualization than raw text output in Debug Console
  3. Exception Object Parsing

    • Parse C# Exception objects via DAP API
    • Display Message, StackTrace, InnerException, Source, HResult, Data in a structured format
    • Make all stack traces clickable with accurate line navigation

Extension Repository: https://gitee.com/odinsam/debug-window

The Critical Issue: Cannot Access Variables in ASP.NET Core WebAPI

When implementing Exception parsing, I discovered that DAP API cannot access ANY variables during ASP.NET Core HTTP request processing, while the built-in Debug Console works perfectly.

Environment

  • OS: macOS 24.6.0
  • VSCode: Latest
  • C# Extension: dotnet.vscode-csharp (latest)
  • .NET: 8.0
  • Debugger Type: coreclr

Reproduction Steps

Step 1: Create an ASP.NET Core WebAPI project with a service layer

Step 2: Add exception handling in a service method:

public class TestService : ITestService
{
    public bool ProcessData(string data)
    {
        try
        {
            var x = 10;
            var y = 0;
            var z = x / y;  // Trigger DivideByZeroException
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");  // ⭐ Set breakpoint here
            throw;
        }
    }
}

Step 3: Call this service from a Controller during an HTTP request:

[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
    private readonly ITestService _testService;

    public TestController(ITestService testService)
    {
        _testService = testService;
    }

    [HttpGet("error")]
    public IActionResult TriggerError()
    {
        try
        {
            var result = _testService.ProcessData("error");
            return Ok(new { success = result });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { success = false, message = ex.Message });
        }
    }
}

Step 4: Start debugging (F5) and make an HTTP request to the endpoint

Step 5: When breakpoint is hit:

  • Built-in Debug Console: Type ex → ✅ Works perfectly, displays full exception details
  • My extension using DAP API: → ❌ Fails, cannot retrieve any variables

DAP API Behavior Comparison

✅ In Console Applications (Works Perfectly)

// Scopes request
{
  "scopes": [{
    "name": "Locals",
    "variablesReference": 1008,
    "expensive": false
  }]
}

// Variables request (returns variables!)
{
  "variables": [
    {
      "name": "ex",
      "type": "System.DivideByZeroException",
      "value": "{System.DivideByZeroException: Attempted to divide by zero...}",
      "variablesReference": 1009
    },
    {
      "name": "x",
      "type": "int",
      "value": "10",
      "variablesReference": 0
    },
    {
      "name": "y",
      "type": "int",
      "value": "0",
      "variablesReference": 0
    }
  ]
}

// Evaluate request succeeds
{
  "result": "{System.DivideByZeroException: Attempted to divide by zero...}",
  "type": "System.DivideByZeroException",
  "variablesReference": 1009
}

❌ In ASP.NET Core WebAPI HTTP Request Handling (Fails)

// Scopes request (looks normal)
{
  "scopes": [{
    "name": "Locals",
    "variablesReference": 1008,  // ⚠️ Non-zero reference
    "expensive": false
  }]
}

// Variables request (EMPTY despite non-zero reference!)
{
  "variables": []  // ❌ Empty array
}

// Evaluate request attempts (ALL FAIL):

// Without context, without frameId
Error: "Evaluation in global scope is not supported."

// Without context, with frameId
Error: "Evaluation failed."

// With context: 'repl' and frameId
Error: "Evaluation failed."

// With context: 'watch' and frameId
Error: "Evaluation failed."

// With context: 'hover' and frameId
Error: "Evaluation failed."

Extensive Testing Results

I created comprehensive test projects to isolate the root cause:

Test Scenario IoC Container AOP Proxy Async Method HTTP Request DAP Variables Access
Console - basic Works
Console - in throw statement Works
Console - class method Works
Console - Autofac+Castle.DynamicProxy Works
WebAPI - Autofac+AOP+Async Fails
WebAPI - Autofac+AOP+Sync Fails
WebAPI - Autofac only (no AOP) Fails
WebAPI - Built-in DI (no Autofac) Fails
WebAPI - No DI at all Fails
WebAPI - Startup code (before HTTP pipeline) Works

Key Finding: The issue is specific to ASP.NET Core's HTTP request processing pipeline, completely unrelated to:

  • ❌ Code optimization (Debug vs Release mode - tested both)
  • ❌ IoC containers (tested with Autofac, built-in DI, and no DI)
  • ❌ AOP dynamic proxies (tested with and without Castle.DynamicProxy)
  • ❌ Async vs sync methods (both fail)
  • ❌ Project configuration or debugger settings (launch.json, .csproj)

Code Used for DAP Requests

// Get frame ID
const threadsResponse = await debugSession.customRequest('threads', {});
const thread = threadsResponse.threads[0];
const stackTrace = await debugSession.customRequest('stackTrace', {
    threadId: thread.id
});
const frameId = stackTrace.stackFrames[0].id;

// Method 1: Variables request
const scopesResponse = await debugSession.customRequest('scopes', {
    frameId: frameId
});
// Returns: { scopes: [{ name: "Locals", variablesReference: 1008 }] }

const variablesResponse = await debugSession.customRequest('variables', {
    variablesReference: scopesResponse.scopes[0].variablesReference
});
// Returns: { variables: [] } ❌ Empty in WebAPI HTTP context

// Method 2: Evaluate request (multiple attempts, all fail)
const response = await debugSession.customRequest('evaluate', {
    expression: 'ex',
    frameId: frameId,
    context: 'repl'  // Also tried: 'watch', 'hover', undefined
});
// Throws: Error "Evaluation failed."

Why This Is Critical

  1. Debug Console works, DAP API doesn't

    • This inconsistency suggests Debug Console might be using a different internal API
    • Extensions cannot provide the same functionality as built-in tools
  2. Affects advanced debugging tools

    • Extensions cannot build enhanced debugging experiences for WebAPI projects
    • Limits innovation in the extension ecosystem
  3. No documentation about this limitation

    • Developers waste time trying to fix what might be an architectural limitation
    • Should be documented if this is expected behavior

Current Workaround

I now show a helpful error message guiding users to the Debug Console:

❌ Cannot retrieve variable: ex

🔍 Reason: During ASP.NET Core HTTP request processing, debugger variable access is restricted

💡 Best Solution:
  ▸ Press Ctrl+Shift+Y (Windows) or Cmd+Shift+Y (Mac) to open Debug Console
  ▸ Type in Debug Console: ex
  ▸ Debug Console is not affected by this limitation

Other Options:
  • View left-side VARIABLES panel
  • Add logging: Console.WriteLine()

Questions for the Team

  1. Is this expected behavior or a bug?

    • If expected: Please document this limitation in the extension development guide
    • If a bug: I'm happy to provide more debugging information
  2. Why does Debug Console work differently?

    • Does it bypass DAP and communicate directly with the debugger?
    • Can this mechanism be exposed to extensions?
  3. Are there alternative DAP methods for this scenario?

    • Special request types?
    • Different parameters?
    • Undocumented APIs?
  4. Root cause in the debugger:

    • Is this a limitation of the .NET debugger (vsdbg)?
    • Or the VSCode C# extension's implementation?
    • Related to ASP.NET Core's threading model?

Impact

This limitation affects any VSCode extension that attempts to:

  • Build custom debug views
  • Parse and visualize complex variables
  • Provide alternative debugging interfaces

Understanding and potentially resolving this would greatly benefit the extension development ecosystem.


Thank you for your time! I'm happy to provide additional information, test projects, or help debug this issue. 🙏

Extension Repository: https://github.com/odinGitGmail/DeBugWindow

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions