Under the hood with debugging in Blazor WebAssembly

This blog post is a detour from the typical content I’ve been writing about Blazor, but it is still one that is compelling to cover.

If you’re familiar with Blazor, you might be aware that there are two different hosting models for Blazor:

Whenever you debug a Blazor Server application, you’re debugging using the standard .NET debugger. However, the same doesn’t apply for Blazor WebAssembly since it is running in the browser. It turns out that an interesting assortment of technologies come into play to get a comparable debugging experience working for Blazor on WebAssembly.

The Chrome DevTools Protocol

Let’s say you’ve created a new Blazor WASM application using the CLI.

$ dotnet new blazorwasm -o DebuggingTest
$ cd DebuggingTest
$ dotnet run

To initiate debugging within the browser, we use the Alt+Shift+D. At this point, it’s likely that you’ll get screen instructing you to launch a new Chromium browser with the --remote-debugging argument provided.

Screen Shot 2020-11-08 at 7 45 41 PM

This alert hints at one of the first key points we’ll cover: the Chrome DevTools Protocol. Chrome DevTools are a client-side interface included with Chromium-browsers that allows developers to inspect elements, browse the source files associated with a web app, and debug an application. DevTools communicates with the page containing a running application using a serialized messaging protocol known as the Chrome DevTools Protocol. The protocol defines messages like DOM.highlightNode to highlight particular DOM nodes on the page and Debugger.setBreakpoint to set the debugger at a particular location in the source file.

The Blazor WebAssembly debugger piggybacks on this protocol to faciliate debugging a Blazor WASM application running in the browser.

The remote-debugging property allows us to configure a port that DevTools services running outside the browser. If you’ve ever attached a debugger to a Node process or debugged a JavaScript application from an editor, you’ve used this infrastructure. The Blazor WASM debugging experience piggy backs on the same thing.

The Debugging Proxy

So far, we have a way for an editor to communicate with our application over a standardized debugger. This works fine for a plain, old JavaScript application where we are attempting to set breakpoints on files that aren’t transpiled or modified otherwise from their on-disk versions. The story is a lot different when we are attempting to debug an application that consists of C# code running in a browser via a runtime targeting WASM.

To resolve this, the debugging experience in Blazor WebAssembly contains a proxy component that sits between the browser and the editor.

The debugging proxy is a separate server process that is launched when the user activates a debugging session within the browser via the keyboard shortcut or initializes the debugger from VS.

If you’ve debugged a Blazor WASM application in Visual Studio or Visual Studio Code, you might be familiar with with the inspectUri parameter that is set in the launchSettings.json file.

"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"

The inspectUri parameter provided the WebSocket address of the debugging proxy that the IDE should communicate with when interfacing with the debugger.

Inside the Debugging Proxy

Once the debugging proxy has been initialized, it loads all of the PDBs that are associated with a particular Blazor WASM application as source files that are recognized by the DevTools instance.

Screen Shot 2020-11-08 at 9 31 57 PM

For the remainder of the debugging session, debugging is facilitated by the debugging proxy. When the editor sends a setBreakpointByUrl request like the following:

{
    id: 0,
    method: "Debugger.setBreakpointByUrl",
    params: {
        lineNumber: 10,
        url: "file://Users/captainsafia/verifications/DebuggingTest/Pages/Counter.razor",
        columnNumber: 8
    }
}

The debugging proxy intercepts this message, sets the breakpoint, and sends back a Debugger.breakpointResolved message to the DevTools client to let it know that the breakpoint was set. What does “set the breakpoint” mean in the ordering above?

The “real debugger” in this experience is the Mono debugger which is implemented as part of the runtime. Setting the breakpoint requires interfacing with the debugger to set the breakpoint so that execution is paused the breakpoint is hit.

Conclusion

So that’s that on that. One of the things I love about this implemenetation is that it bridges together a lot of different concepts from different messaging protocols to different communication protocols and so on. Overall: