{"id":399743,"date":"2023-11-07T13:38:00","date_gmt":"2023-11-07T12:38:00","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=dotnet&#038;p=399743"},"modified":"2023-11-07T16:59:22","modified_gmt":"2023-11-07T15:59:22","slug":"how-jetbrains-rider-implemented-net-webassembly-debugging","status":"publish","type":"dotnet","link":"https:\/\/blog.jetbrains.com\/en\/dotnet\/2023\/11\/07\/how-jetbrains-rider-implemented-net-webassembly-debugging","title":{"rendered":"How JetBrains Rider Implemented .NET WebAssembly Debugging"},"content":{"rendered":"\n<p><a href=\"https:\/\/dotnet.microsoft.com\/en-us\/apps\/aspnet\/web-apps\/blazor\" target=\"_blank\" rel=\"noopener\">Blazor<\/a> is part of a .NET technology that lets you build full-stack web applications using C# without the need to write JavaScript code. There\u2019s server-side Blazor, client-side Blazor (which uses WebAssembly (WASM) to run in the browser and interact with the DOM), and <a href=\"https:\/\/learn.microsoft.com\/en-us\/aspnet\/core\/blazor\/hosting-models?view=aspnetcore-8.0\" target=\"_blank\" rel=\"noopener\">other hosting models<\/a>.<\/p>\n\n\n\n<p>Our .NET IDE, <a href=\"https:\/\/www.jetbrains.com\/rider\/\" target=\"_blank\" rel=\"noopener\">JetBrains Rider<\/a>, helps you develop Blazor applications. You can write code and use the debugger to run and troubleshoot the apps you are developing. While the process of implementing a debugger is more or less the same for Blazor Server as it is for any other .NET application, the debugger implementation for Blazor WASM is quite different.<\/p>\n\n\n\n<p>In this blog post, we\u2019ll look at some of the finer engineering points of how we implemented the IDE frontend for debugging both Blazor WASM and all variations of .NET apps targeting WebAssembly!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The .NET WebAssembly family<\/h2>\n\n\n\n<p>Before Blazor, there were several other well-known and more obscure frameworks, both from Microsoft and third-party authors, that allowed users to run .NET in the browser. For example, there is <a href=\"https:\/\/opensilver.net\/\" target=\"_blank\" rel=\"noopener\">OpenSilver<\/a>, an open-source implementation of the now deprecated Silverlight. For a while, there was also a Bridge.NET framework.<\/p>\n\n\n\n<p>With Blazor, Microsoft released the first first-party framework for .NET in the browser using WebAssembly technology. Blazor WebAssembly can be used to develop Single-Page Applications (SPAs). These can be hosted by the DevServer that is part of the .NET SDK, or as part of an ASP.NET Core backend running server-side APIs to build full-stack applications with .NET.<\/p>\n\n\n\n<p>When .NET 7 was released, two new options appeared as part of the new SDK\u2019s <code>wasm-experimental<\/code> workload: the <code>browser-wasm<\/code> and <code>console-wasm<\/code> runtime identifiers (RID). These let you target WASM in the browser with Blazor, and NodeJS in the terminal for other .NET application types.<\/p>\n\n\n\n<p>Based <a href=\"https:\/\/github.com\/SteveSandersonMS\/dotnet-wasi-sdk\" target=\"_blank\" rel=\"noopener\">on the <code>Wasi.Sdk<\/code> prototype<\/a> created by Steve Sanderson, .NET 8 will include another workload that targets the WebAssembly System Interface (WASI), which will make it possible to run .NET code through WebAssembly outside of the browser while maintaining access to the file system, network, system calls, and more.<\/p>\n\n\n\n<p><em>Tip: Our developer advocate Khalid Abuhakmeh covered a number of these options in t<\/em><a href=\"https:\/\/blog.jetbrains.com\/en\/dotnet\/2022\/12\/15\/the-future-of-net-with-wasm\"><em>he Future of .NET with WASM<\/em><\/a><em>.<\/em><\/p>\n\n\n\n<p>Typically, these approaches use the Mono Ahead of Time (AOT) compiler to generate WebAssembly binaries. There\u2019s an <a href=\"https:\/\/github.com\/dotnet\/runtimelab\/tree\/feature\/NativeAOT-LLVM\" target=\"_blank\" rel=\"noopener\">experimental NativeAOT-LLVM project<\/a> by the .NET team that uses the Emscripten toolchain, but it is currently not ready for use in real projects.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Anatomy of a .NET WebAssembly application<\/h2>\n\n\n\n<p>Let\u2019s look at how .NET applications targeting WebAssembly are composed. We\u2019ll start with Blazor WASM and then dig into the <code>wasm-experimental<\/code> workload.&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Blazor WASM<\/h3>\n\n\n\n<p>When creating a Blazor WASM application, you will find an <code>index.html<\/code> file which, like in most other SPA frameworks, contains a <code>div<\/code> element in which the framework will create and render your application. There will also be a <code>script<\/code> element that loads the Blazor WebAssembly framework:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"4,8\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;!DOCTYPE html>\n&lt;!-- ... -->\n&lt;body>\n  &lt;div id=\"app\">\n    \/\/ ...\n  &lt;\/div>\n  \/\/ ...\n  &lt;script src=\"_framework\/blazor.webassembly.js\">&lt;\/script>\n&lt;\/body>\n&lt;\/html><\/pre>\n\n\n\n<p>After building this project, you will see that several files are created:<\/p>\n\n\n\n<ul>\n<li><code>BlazorApp1.dll<\/code> and <code>BlazorApp1.pdb<\/code> \u2013 A compiled version of your application.<\/li>\n\n\n\n<li><code>blazor.boot.json<\/code> \u2013 Contains information about the entry assembly (<code>BlazorApp1<\/code>) and information about the runtime, dependencies, and so on.<\/li>\n\n\n\n<li><code>dotnet.wasm<\/code>  \u2013 A version of the .NET runtime (Mono, to be more precise) compiled into a WebAssembly module.<\/li>\n\n\n\n<li><code>mscorlib.dll<\/code> \u2013 The .NET framework core libraries, compiled into Common Intermediate Language (CIL).<\/li>\n\n\n\n<li><code>blazor.webassembly.js<\/code> \u2013 A file that glues all of the above together.<\/li>\n<\/ul>\n\n\n\n<p>If you run this application using the <code>dotnet run<\/code> command, for example, you\u2019ll see a <code>dotnet<\/code> process begins. This then starts (by default) the DevServer and hosts your application, which you can work with through the browser:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">dotnet run\n\u2514\u2500\u2500 dotnet: \"~\\.nuget\\packages\\microsoft.aspnetcore.components.webassembly.devserver\\7.0.5\/tools\/blazor-devserver.dll\" --applicationpath \"...\\BlazorApp1\\bin\\Debug\\net7.0\\BlazorApp1.dll\"<\/pre>\n\n\n\n<p>Now let\u2019s see how the <code>wasm-experimental<\/code> workload handles this.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">wasm-experimental<\/h3>\n\n\n\n<p>To try out the <code>wasm-experimental<\/code> workload, you\u2019ll first need to install it:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">dotnet workload install wasm-tools wasm-experimental<\/pre>\n\n\n\n<p>Once done, you can create a new .NET application using the <code>wasmbrowser<\/code> template:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">dotnet new wasmbrowser --name WasmApp1<\/pre>\n\n\n\n<p>Like with Blazor WASM, an <code>index.html<\/code> file is created with a <code>span<\/code> element used for rendering. The HTML file also loads the&nbsp;<code>main.js<\/code> file as a script, not a Blazor framework-related script like in the previous example.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"html\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"5,8\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;!DOCTYPE html>\n&lt;html>\n&lt;head>\n  &lt;!-- ... -->\n  &lt;script type='module' src=\".\/main.js\">&lt;\/script>\n&lt;\/head>\n&lt;body>\n  &lt;span id=\"out\">&lt;\/span>\n&lt;\/body>\n&lt;\/html><\/pre>\n\n\n\n<p>The <code>main.js<\/code> that is loaded is very different from the Blazor approach, where there\u2019s little control over how the application is launched. In the <code>main.js<\/code>, we can see that .NET is imported, some other code is executed, and finally, <code>dotnet.run()<\/code> is invoked to start the application.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { dotnet } from '.\/dotnet.js'\n\n\/\/ ...\n\nawait dotnet.run();<\/pre>\n\n\n\n<p>The <code>...<\/code> in the above code snippet is important. During startup, you can change the .NET (well, Mono) runtime configuration, such as network download policy settings and more. When using WebGL, for example, you can specify the canvas for rendering. You could also change the logging level, which may be useful for seeing what\u2019s going on when you are porting your application to .NET WebAssembly.<\/p>\n\n\n\n<p>After building this project, you\u2019ll find that several files are created:<\/p>\n\n\n\n<ul>\n<li><code>mono-config.json<\/code> \u2013 Metadata generated from your project, specifying the main assembly name, assembly folder, debug level, sources, asset hashes, and more.<\/li>\n\n\n\n<li><code>managed\/<\/code> (folder) \u2013 Your application\u2019s managed assemblies.<\/li>\n\n\n\n<li><code>dotnet.js<\/code> and <code>dotnet.js.symbols<\/code> \u2013 JavaScript-based APIs to configure and manipulate the Mono runtime together with debug symbols.<\/li>\n\n\n\n<li><code>dotnet.wasm<\/code> \u2013 A version of the .NET runtime (Mono, to be more precise) compiled into a WebAssembly binary.<\/li>\n\n\n\n<li><code>index.html<\/code> and <code>main.js<\/code> \u2013 The files we saw earlier, bootstrapping your application.<\/li>\n\n\n\n<li><code>WasmApp1.runtimeconfig.json<\/code> \u2013 A runtime configuration file, which is required since <code>wasm-experimental<\/code> projects add a new runtime identifier.<\/li>\n<\/ul>\n\n\n\n<p>The <code>WasmApp1.runtimeconfig.json<\/code> contains metadata for the runtime to help determine how the application should be run. It specifies the main assembly, runtime arguments if needed, and an <code>index.html<\/code> file that will be run in the browser.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">{\n  \"runtimeOptions\": {\n    \"tfm\": \"net8.0\",\n    \"wasmHostProperties\": {\n      \"perHostConfig\": [\n        {\n          \"name\": \"browser\",\n          \"html-path\": \"index.html\",\n          \"Host\": \"browser\"\n        }\n      ],\n      \"runtimeArgs\": [],\n      \"mainAssembly\": \"WasmApp1.dll\"\n    },\n  }\n}<\/pre>\n\n\n\n<p>To run the application, you can invoke <code>dotnet run<\/code> again. In the process tree, you\u2019ll notice <code>WasmAppHost<\/code> as the application (and not the DevServer from earlier):<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">dotnet: run\n\u2514\u2500\u2500 dotnet: exec \"C:\\Program Files\\dotnet\\packs\\Microsoft.NET.Runtime.WebAssembly.Sdk\\8.0.0-preview.4.23259.5\\WasmAppHost\\WasmAppHost.dll\" --runtime-config \"D:\\Playground\\WasmApp1\\WasmApp1\\bin\\Debug\\net8.0\\browser-wasm\\AppBundle\\WasmApp1.runtimeconfig.json\"<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">In summary<\/h3>\n\n\n\n<p>Whether you use Blazor WASM or the <code>wasm-experimental<\/code> workload, almost any .NET WebAssembly application runs with the Mono runtime. It\u2019s important to note, however, that even though you specified a compatible version of the language and the target framework, some C# functionality that you compile will not be understood by the runtime. A good example is generic types in attributes, introduced in C# 11. You can use such an attribute in your code, but at the time of writing this post, it is not supported by the Mono runtime and fails to execute.<\/p>\n\n\n\n<p><em>Note: Generic attributes in Mono <\/em><a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/77047\" target=\"_blank\" rel=\"noopener\"><em>will work starting from .NET 8<\/em><\/a><em>.<\/em><\/p>\n\n\n\n<p>In addition to the Mono runtime, we saw that your application will be hosted with the DevServer, the WasmAppHost, or on ASP.NET Core. You will find an <code>index.html<\/code> file that is opened in the browser, as well as some JavaScript glue code.<\/p>\n\n\n\n<p>Now, how does the debugger in Rider communicate with the runtime? And how does the runtime itself communicate with the browser (and vice versa)? Let\u2019s find out!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Debugging a .NET desktop application<\/h2>\n\n\n\n<p>Before we dive into debugging .NET WebAssembly applications, let\u2019s take a quick detour and talk about debugging a .NET desktop application. There are three actors involved in this process \u2013 a runtime (which runs your application&#8217;s code), the debugger client (Rider), and your application. When the runtime starts, it waits for the debugger to connect to it, and then continues executing the code.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1240\" height=\"192\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2023\/10\/image-31.png\" alt=\"\" class=\"wp-image-399748\"\/><\/figure>\n\n\n\n<p>When debugging locally, with both the debugger and your application on the same machine, the debugger client is almost always in the same environment as the runtime, so the lifetimes of both actors are closely related. When you close your application, the debugger knows it can stop running.<\/p>\n\n\n\n<p>For .NET WebAssembly applications, the debugger&#8217;s and debuggee&#8217;s lifetime (your application) becomes more\u2026 interesting.<\/p>\n\n\n\n<p>First of all, there\u2019s a Chromium-based browser involved. The browser hosts the <code>index.html<\/code> page as a tab. This browser tab hosts a regular WebAssembly runtime that can execute special <code>.wasm<\/code> code. Meanwhile, the WASM runtime starts the Mono runtime, which can decode and execute .NET\u2019s Common Intermediate Language (CIL) in the form of an assembly.&nbsp;<\/p>\n\n\n\n<p>As part of this runtime, your application code is executed \u2013 the part that we\u2019re interested in debugging. A rather complex onion-like architecture emerges, through which the debugger needs to be able to monitor the code execution process and receive events in the reverse direction, for example, when a breakpoint is hit.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1333\" height=\"585\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2023\/10\/image-32.png\" alt=\"\" class=\"wp-image-399759\"\/><\/figure>\n\n\n\n<p>After this short interruption, let\u2019s look at how JetBrains Rider works with all of these!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The debug proxy<\/h2>\n\n\n\n<p>Luckily for us, there\u2019s an existing mechanism for working with an architecture like we just described: the Mono Debug Proxy!<\/p>\n\n\n\n<p>If you go back to the Blazor WASM process tree when running your application, you\u2019ll see that it looks a bit different when you\u2019re running the debugger through Rider. A new child process is started:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Rider.Backend.exe: \u2026\n\u2514\u2500\u2500 winpty-agent.exe: \u2026\n    \u2514\u2500\u2500 dotnet:\n~\/.nuget\/packages\/microsoft.aspnetcore.components.webassembly.devserver\/7.0.5\/tools\/blazor-devserver.dll --applicationpath bin\\Debug\\net7.0\\BlazorApp1.dll\n        \u2514\u2500\u2500 dotnet: exec\n\"~\\.nuget\\packages\\microsoft.aspnetcore.components.webassembly.devserver\\7.0.5\\tools\\BlazorDebugProxy\\BrowserDebugHost.dll\" --OwnerPid 16152 --DevToolsUrl http:\/\/127.0.0.1:64069<\/pre>\n\n\n\n<p>The last process in this tree launches the <code>BrowserDebugHost.dll<\/code>, which receives its parent process ID and a value for the <code>DevToolsUrl<\/code> argument. This URL is one of the key elements for making the debugger proxy work. Let\u2019s add it to our diagram:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1165\" height=\"579\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2023\/10\/image-33.png\" alt=\"\" class=\"wp-image-399770\"\/><\/figure>\n\n\n\n<p>When the Debug Proxy starts, it uses the Chrom(ium) developer tools URL to retrieve the information it needs in order to work with the browser tab. This includes, for example, how to send and receive events from the browser and how to work with the Mono runtime that hosts your application. The Debug Proxy does a lot of heavy lifting for the debugger client, in our case JetBrains Rider, because now it only needs to work with one entity.<\/p>\n\n\n\n<p>In other words, the debugger client (JetBrains Rider) does not work directly with the browser tab. Instead, it connects to the Debug Proxy, which serves as a communication layer between the browser, the debugger client, and the runtime.<\/p>\n\n\n\n<p>As a debugger client, JetBrains Rider can now send and receive calls and events from the browser, for example downloading assets, navigating in the address bar, and so on. The Debug Proxy also listens for JavaScript events from Mono and can work with it through the .NET JavaScript API found in the <code>dotnet.js<\/code> file. Unfortunately, this API is currently undocumented, and changes quite often.<\/p>\n\n\n\n<p>Time for some reverse engineering!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Connecting all the components: Rider, Debug Proxy, and the browser<\/h2>\n\n\n\n<p>To communicate with the Mono runtime running in the browser, JetBrains Rider, the Debug Proxy, and the browser need to communicate with each other. This communication starts with a handshake, ensuring all components know where the other components can be reached.<\/p>\n\n\n\n<p>The handshake that establishes communication between all the components is the most complex and unstable process in the entire mechanism. Let\u2019s start with a diagram so that you can follow along.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"670\" height=\"720\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2023\/10\/image-34.png\" alt=\"\" class=\"wp-image-399781\"\/><\/figure>\n\n\n\n<p>When you start debugging a .NET WebAssembly application, for example <code>WasmApp1<\/code>, the debugger will launch the <code>WasmApp1<\/code> process using the DevServer or ASP.NET Core, depending on the hosting option. The Debug Proxy is also launched as a child process, thanks to the <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/microsoft.aspnetcore.builder.webassemblynetdebugproxyappbuilderextensions.usewebassemblydebugging\" target=\"_blank\" rel=\"noopener\">WebAssembly debugging middleware<\/a> that is registered automatically as part of your application.<\/p>\n\n\n\n<p>Next, a Chromium browser such as Google Chrome or Microsoft Edge is launched and instructed to open a special placeholder URL like <code>about:blank?realUrl=...<\/code>. The browser then writes its debugging port and path to a file in your user profile directory.<\/p>\n\n\n\n<p>The debugger then constructs the debugger endpoint websocket URL based on the port and path from this file. It will then look like this: <code>ws:\/\/127.0.0.1:{port}\/{path}<\/code>. In the next step, the debugger sends an HTTP <code>GET<\/code> request to a special endpoint \u2013 <code>GET http:\/\/localhost:5170\/_framework\/debug\/ws-proxy?browser=ws:\/\/127.0.0.1:{port}\/{path}<\/code> \u2013 which, as you can see, contains the generated websocket URL. The WebAssembly debugging middleware we discussed earlier passes this on to the Debug Proxy.<\/p>\n\n\n\n<p>After that, the Debug proxy comes into play and initializes a WebSocket connection to the browser using the URL it received. Once connected, it opens a new proxy debugging endpoint and returns the URL to this endpoint with a <code>302 Redirect<\/code> response.<\/p>\n\n\n\n<p>Once JetBrains Rider receives the endpoint URL, which the Debug Proxy then returns, it creates a connection to it. And then, the real work begins!<\/p>\n\n\n\n<p>When connected to the Debug Proxy, JetBrains Rider sends information about all breakpoints that were added by the user in the IDE\u2019s editor. Remember, the browser is still open on a special empty page, so your application is not yet running. The Debug Proxy will keep track of these requests to create breakpoints and will apply them when the Mono runtime is ready to receive them. This approach ensures that a breakpoint on the first line of your <code>Main<\/code> method can be triggered, which might otherwise not be possible because, at this point, the runtime is already executing your application.<\/p>\n\n\n\n<p>After the Debug Proxy knows about these breakpoints, JetBrains Rider triggers browser navigation to the actual application \u2013 yours! This starts the Mono runtime and makes sure your application is executed.<\/p>\n\n\n\n<p>As a final step, the Debug Proxy finishes all activities related to activation of the runtime, loads the required assemblies, and sends a signal to JetBrains Rider that it\u2019s ready for action!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Chrome DevTools Protocol (CDP)<\/h2>\n\n\n\n<p>Browsers using the Chromium engine all implement the so-called <a href=\"https:\/\/chromedevtools.github.io\/devtools-protocol\/\" target=\"_blank\" rel=\"noopener\">Chrome DevTools Protocol (CDP)<\/a>. If you\u2019ve worked with <a href=\"https:\/\/www.selenium.dev\/\" target=\"_blank\" rel=\"noopener\">Selenium<\/a> or other frontend test automation tools, you may have used it yourself. Through the CDP interface, you can do almost anything with the browser and pages \u2013 as long as you stay within the limits of the user&#8217;s security settings.<\/p>\n\n\n\n<p>The CDP is well documented and versioned. Structurally, it consists of what are called <em>domains<\/em>, or <em>modules<\/em>, which in turn consist of <em>types<\/em>. Think of them like a structure or record that can transfer specific data. Some <em>methods<\/em> allow the debugger client to make calls into the Chromium instance, and <em>events<\/em>, where the debugger client can subscribe to calls from the browser into the debugger. The Debug Proxy can be considered a &#8220;virtual&#8221; Chromium browser, because it also communicates using the CDP.<\/p>\n\n\n\n<p><em>Note: If you want to explore the CDP protocol in detail, check out the <\/em><a href=\"https:\/\/chromedevtools.github.io\/devtools-protocol\/\" target=\"_blank\" rel=\"noopener\"><em>official CDP documentation<\/em><\/a><em> or <\/em><a href=\"https:\/\/vanilla.aslushnikov.com\/\" target=\"_blank\" rel=\"noopener\"><em>a simplified version<\/em><\/a><em>. Since we had to reverse-engineer the <code>DotnetDebugger<\/code>, <code>Mono<\/code>, and <code>Runtime<\/code> domains of the CDP, we are hosting <\/em><a href=\"https:\/\/mono-cdp.seclerp.me\/\" target=\"_blank\" rel=\"noopener\"><em>the CDP documentation<\/em><\/a><em> for anyone who is interested in writing their own .NET WebAssembly debugger.<\/em><\/p>\n\n\n\n<p>At the transport level, websockets are being used. The messages that flow over this socket are a variation on <a href=\"https:\/\/www.jsonrpc.org\/specification\" target=\"_blank\" rel=\"noopener\">JSON-RPC 2.0<\/a>, which is a format describing remote calls using JSON payloads. This format is also used in the <a href=\"https:\/\/langserver.org\/\" target=\"_blank\" rel=\"noopener\">Language Server Protocol<\/a>.&nbsp;<\/p>\n\n\n\n<p>There are three types of messages: requests, responses and events. Request and response messages from and to the debugger client (JetBrains Rider) are ordered by <code>id<\/code>, whereas events can be triggered at any point in time without strict ordering. Here are some example messages:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"json\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ request\n{\"id\":10, \"method\": \"Page.navigate\", \"params\":{\"url\":\"http:\/\/localhost:5170\/\"}}\n\n\/\/ response\n{\"id\":10, \u201cresult\u201d: {\"frameId\":\"\u2026\",\"loaderId\":\"\u2026\"}}\n\n\/\/ event\n{\"method\": \"Network.requestServedFromCache\", \"params\":{\"requestId\":\"98279.21\"}}<\/pre>\n\n\n\n<p>With CDP, it\u2019s also possible to create <em>sessions <\/em>on a single connection, to control different browser tabs while re-using the websocket connection.<\/p>\n\n\n\n<p><em>Targets <\/em>are another supported concept. The debugger can attach to the browser and a page, but also to service workers, background pages, and so on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">.NET WebAssembly Chrome DevTools Protocol in Rider<\/h2>\n\n\n\n<p>Whew, what a title! Working with the CDP to interact with the Mono runtime running your WebAssembly application is great! However, we found that working with the protocol messages directly gets boring rather quickly. So, we built an abstraction that we can use in the JetBrains Rider code base!<\/p>\n\n\n\n<p>Since several folks on our team may be interacting with the .NET WebAssembly debugger, we wanted to create a simple API that doesn&#8217;t require our entire development team to know about all of the details of the CDP protocol and other machinery we covered in this post.. Here\u2019s an example code snippet of setting up the connection to the browser\u2019s websocket URL, which abstracts much of the handshake we saw earlier in this post.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ Creating connection\nvar connection = new DefaultProtocolClient(new Uri(\"ws:\/\/localhost:5151\"), logger);\nawait connection.ConnectAsync(cancellationToken);\n\n\/\/ Sending commands\nvar response = await connection.SendCommandAsync(\n    Domains.DotnetDebugger.SetDebuggerProperty(\n        JustMyCodeStepping: true\n    )\n);\n\n\/\/ Firing commands (when we're not interested in response)\nawait connection.FireCommandAsync(Domains.Debugger.StepOut());<\/pre>\n\n\n\n<p>The commands sent with this API correspond to methods in CDP terminology. We can send them and wait for a response or make a fire-and-forget call. The commands (methods), events, and types are generated from the JSON specification of the CDP.<\/p>\n\n\n\n<p>In addition to sending messages, it is also possible to listen for events and create new sessions (called \u201cscopes\u201d in the API). We try to avoid using the exact CDP terminology because we will also extend this API to the Firefox Debugger Protocol in the future.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ Listening for events\npageClient.ListenEvent&lt;Domains.Debugger.BreakpointResolved>(async e =>\n{\n    ResolveBreakpoint(e.BreakpointId.Value);\n});\n\n\n\/\/ Creating scoped clients (clients for specific sessions)\nvar scopedClient = connection.CreateScoped(sessionId);<\/pre>\n\n\n\n<p>Since methods must be ordered in the CDP, they are not sent directly. Instead, they are added to a queue. In a long-running task, they are sent with an incremental message ID. When sending, a <code>TaskCompletionSource<\/code> is created for the same message id, so that we can <code>await<\/code> the response to this message.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"wpcustom\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">private readonly BlockingCollection&lt;ProtocolRequest&lt;ICommand>> _outgoingMessages = \u2026\n\npublic async Task&lt;TResponse> SendCommandAsync&lt;TResponse>(ICommand&lt;TResponse> command,\n  string? sessionId = null,\n  CancellationToken? token = default) where TResponse : IType\n{\n  var id = Interlocked.Increment(ref _currentId);\n  var resolver = new TaskCompletionSource&lt;JObject>();\n  if (_responseResolvers.TryAdd(id, resolver))\n  {\n    await FireInternalAsync(id, GetMethodName(command.GetType()), command, sessionId);\n    var responseRaw = await resolver.Task;\n    var response = responseRaw.ToObject...\n    return response;\n  }\n  throw new Exception(\"Unable to enqueue message to send\");\n}\n\nprivate async Task FireInternalAsync(int id, string methodName, ICommand command, string? sessionId)\n{\n  var request = new ProtocolRequest&lt;ICommand>(id, methodName, command, sessionId);\n  if (!_outgoingMessages.TryAdd(request)) throw new Exception(\"Can't schedule outgoing message for sending.\");\n}<\/pre>\n\n\n\n<p>A separate long-running task also listens for incoming messages from the browser. These can be both events and responses to a message that was sent earlier. For events, the appropriate delegate is invoked. In the case of responses to messages, we retrieve the corresponding <code>TaskCompletionSource<\/code> created when sending the message, and depending on whether it is a result of an error, we set the <code>TaskCompletionSource<\/code> status.<\/p>\n\n\n\n<p>When setting breakpoints in the CDP, we do so based on the file name. The runtime loads different assemblies and source maps from debug symbols one by one, and as soon as it finds a candidate that matches the file name pattern, a <code>BreakpointResolved<\/code> event is fired. JetBrains Rider has to respond to it as quickly as possible to make sure you don\u2019t see a grayed-out breakpoint in the editor.<\/p>\n\n\n\n<p>A queue is also used to send the commands to create or remove breakpoints, due to the specifics of Rider\u2019s debugger infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Hot reload with .NET WebAssembly<\/h2>\n\n\n\n<p>With hot reload, you can make changes to code while debugging and apply those changes to your application without restarting it. While JetBrains Rider supports hot reload for many application types, it does not (yet!) support hot reload for .NET WebAssembly.<\/p>\n\n\n\n<p>The way hot reload would work with .NET WebAssembly is by making use of the Edit-and-Continue (EnC) functionality in the runtime.<\/p>\n\n\n\n<p><em>Note: If you want to learn more, check out this post about <\/em><a href=\"https:\/\/blog.jetbrains.com\/en\/dotnet\/2021\/12\/02\/how-rider-hot-reload-works-under-the-hood\"><em>how .NET hot reload works in Rider<\/em><\/a><em>.<\/em><\/p>\n\n\n\n<p>The Debug Proxy has added support for EnC in .NET 7. With that now available, consider this short section as a confirmation that we are indeed working on it!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Although we did not have time to go over all debugger-related topics such as evaluating expressions, watches, and working with call stacks, we do hope this blog post gave you a solid rundown of how the .NET WebAssembly debugger works in JetBrains Rider!<\/p>\n\n\n\n<p>Want to join our JetBrains Rider or ReSharper team and help us build the best .NET developer tools on the market? <a href=\"https:\/\/www.jetbrains.com\/careers\/jobs\/\" target=\"_blank\" rel=\"noopener\">We\u2019re hiring!<\/a><\/p>\n\n\n\n<p><em>Photo by <a href=\"https:\/\/unsplash.com\/@clayton_cardinalli?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash\" target=\"_blank\" rel=\"noopener\">Clayton Cardinalli<\/a> on <a href=\"https:\/\/unsplash.com\/photos\/man-in-blue-jacket-standing-beside-brown-wooden-post-hkJNx0EDbjE?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash\" target=\"_blank\" rel=\"noopener\">Unsplash<\/a><\/em><\/p>\n","protected":false},"author":118,"featured_media":399800,"comment_status":"closed","ping_status":"closed","template":"","categories":[4992],"tags":[1237,1978,7120],"cross-post-tag":[6256],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/dotnet\/399743"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/dotnet"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/types\/dotnet"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/users\/118"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/comments?post=399743"}],"version-history":[{"count":10,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/dotnet\/399743\/revisions"}],"predecessor-version":[{"id":403989,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/dotnet\/399743\/revisions\/403989"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/media\/399800"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/media?parent=399743"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/categories?post=399743"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/tags?post=399743"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/en\/wp-json\/wp\/v2\/cross-post-tag?post=399743"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}