Skip to content

Blazor hosting changes for WebAssembly debugging - #18928

Draft
maraf wants to merge 16 commits into
microsoft:mainfrom
maraf:u/maraf/blazor-hosting-debugger
Draft

Blazor hosting changes for WebAssembly debugging#18928
maraf wants to merge 16 commits into
microsoft:mainfrom
maraf:u/maraf/blazor-hosting-debugger

Conversation

@maraf

@maraf maraf commented Jul 29, 2026

Copy link
Copy Markdown
Member

This PR is a subset of POC in #17559. It includes changes to blazor hosting and associated tests.

Description

Adds browser debugging support for Blazor WebAssembly apps to Aspire.Hosting.Blazor:

  • New BrowserDebuggerHelper that registers a hidden child "browser debugger" resource parented to the Blazor gateway and adds a Debug in Browser command to the WASM app resource, and watches for the debugger stopping.
  • AddBlazorGateway / gateway registration now accepts an optional debuggerBrowser parameter (defaults to "msedge", also supports "chrome") that is passed through to the WASM app registration, inspired by the JavaScript app debugger support.
  • BlazorHostedExtensions resolves the Blazor WebAssembly client project path from the server project's references so the debug bridge can locate the WASM BCL assemblies in the client's output directory.
  • Debugger registration is skipped in publish mode.
  • Adds unit tests covering BrowserDebuggerHelper, BlazorHostedExtensions, and the WithBlazorApp flows.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

javiercn and others added 15 commits July 29, 2026 18:57
Implement 'Debug in Browser' command for Blazor WebAssembly apps using
the DCP IdeSession resource model (microsoft/dcp#169):

- BrowserDebugAnnotation: marks resources as browser-debuggable
- IdeSession DCP resource: models debug sessions with lifecycle states
- BrowserDebugLaunchConfiguration: launch config for browser-debug type
- IDcpExecutor/DcpExecutor: CreateIdeSessionAsync + StartIdeSessionAsync
- ExecutableCreator: creates IdeSession at startup, handles 404 gracefully
  when DCP does not yet support IdeSession
- ApplicationOrchestrator: LaunchBrowserDebugSessionAsync coordinator
- BlazorGatewayExtensions: annotation + command for standalone WASM
- BlazorHostedExtensions: auto-registers debug command in EnsureEnvironmentCallback
  (triggered by ProxyBlazorService/ProxyBlazorTelemetry)
- Command visibility gated on IDE connection (DEBUG_SESSION_PORT)
- Suppress CS0436 in Aspire.Hosting.Blazor (pre-existing StringComparers conflict)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove IdeSession DCP resource type (no longer needed)
- Reuse existing BrowserDebuggerResource/ExecutableResource pattern with
  WithExplicitStart() for on-demand browser debug sessions
- Create BrowserDebuggerHelper to consolidate shared logic between
  BlazorHostedExtensions and BlazorGatewayExtensions
- Use BrowserLaunchConfiguration with browser='msedge' for IDE dispatch
- Command handler calls StartResourceAsync directly (removed dead
  LaunchBrowserDebugSessionAsync wrapper)
- Clean up unused code from DcpExecutor, ExecutableCreator, GroupVersion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove duplicate BrowserLaunchConfiguration from Aspire.Hosting.Dcp.Model
- Source-share BrowserDebuggerResource and BrowserLaunchConfiguration from
  Aspire.Hosting.JavaScript via Compile Include links
- Use BrowserDebuggerResource instead of raw ExecutableResource in helper
- Remove public ProxyWasmDebugging methods; debugging is now applied
  automatically and idempotently by EnsureEnvironmentCallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The annotation is only used by Blazor code; it doesn't belong in
the core Aspire.Hosting package.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The annotation only served as an idempotency marker. Replace with a
simple check for an existing resource with the expected debugger name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Register two mutually exclusive commands: 'Debug in Browser' (bug icon)
  and 'Stop Browser Debug' (stop icon) that toggle based on session state
- Resolve DCP instance name from DcpInstancesAnnotation at execution time
  since StartResourceAsync/StopResourceAsync expect the DCP metadata name
- Publish no-op updates on command target to force dashboard re-evaluation
- Watch for debugger resource stop (only after Running state observed) to
  reset session flag when user closes the browser

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…obustness

- Extension sends sessionTerminated to DCP when browser debug session ends
  (onDidTerminateDebugSession in browser.ts → AspireDebugSession.sendSessionTerminated)
- BrowserDebuggerHelper state machine improvements:
  - Double-click guard on debug command
  - CancellationTokenSource for watcher lifecycle
  - Terminal state detection: Finished, Terminated, Exited, FailedToStart, NotStarted
- Convert BlazorHosted playground to global interactivity (client-side routing)
- Add WASM debug settings to BlazorHosted.Client.csproj

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…stration (inspired by javascript app debugger support)
…stration (inspired by javascript app debugger support)
Copilot AI review requested due to automatic review settings July 29, 2026 17:03
@github-actions github-actions Bot added the area-integrations Issues pertaining to Aspire Integrations packages label Jul 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 18928

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 18928"

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a run-only browser-debugger companion for hosted and gateway Blazor WebAssembly applications.

Changes:

  • Adds debugger resources, dashboard commands, and launch configuration.
  • Resolves hosted WASM client project paths.
  • Adds unit coverage for debugger registration and lifecycle behavior.

Reviewed changes

Copilot reviewed 20 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Aspire.Hosting.Blazor/BrowserDebuggerHelper.cs Implements debugger resource and commands.
src/Aspire.Hosting.Blazor/BlazorHostedExtensions.cs Adds hosted-client debugger integration.
src/Aspire.Hosting.Blazor/BlazorGatewayExtensions.cs Adds gateway debugger configuration.
src/Aspire.Hosting.Blazor/Aspire.Hosting.Blazor.csproj Adds shared debugger sources.
tests/Aspire.Hosting.Blazor.Tests/BrowserDebuggerHelperTests.cs Tests debugger-stop observation.
tests/Aspire.Hosting.Blazor.Tests/BlazorHostedExtensionsTests.cs Tests hosted debugger integration.
tests/Aspire.Hosting.Blazor.Tests/WithBlazorAppTests.cs Tests gateway debugger integration.
tests/Aspire.Hosting.Blazor.Tests/Aspire.Hosting.Blazor.Tests.csproj Links shared async test helpers.
Comments suppressed due to low confidence (5)

src/Aspire.Hosting.Blazor/BrowserDebuggerHelper.cs:47

  • These derived names can exceed Aspire's 64-character resource-name limit even when both user-supplied names are individually valid. AddResource validates the generated name and will reject, for example, two 28-character names plus this suffix. Apply the established NameValidationPolicyAnnotation.None policy used by other internal suffixed helpers such as project rebuilders.
        var debuggerResourceName = relativePath is not null
            ? $"{parentResource.Name}-{commandTarget.Resource.Name}-debugger"
            : $"{parentResource.Name}-wasm-debugger";

src/Aspire.Hosting.Blazor/BrowserDebuggerHelper.cs:96

  • web_root is copied directly to the VS Code pwa-* debug configuration's webRoot (extension/src/debugger/languages/browser.ts:24-30), which requires a directory. Passing the .csproj file path makes source-map resolution root beneath a file and prevents the debugger from locating sources. Use the already computed clientProjectDir here and update the assertions accordingly.
                        WebRoot = clientProjectPath,

src/Aspire.Hosting.Blazor/BrowserDebuggerHelper.cs:111

  • This check does not serialize command execution. ResourceCommandService invokes callbacks without a per-resource lock, so two concurrent requests can both observe false, start the debugger twice, and concurrently cancel/dispose/reassign watcherCts; one request can then access a disposed CTS. Guard the session state and CTS transitions with a semaphore or other atomic synchronization before the first await.
                if (debugSessionActive)
                {
                    return CommandResults.Success();
                }

src/Aspire.Hosting.Blazor/BrowserDebuggerHelper.cs:168

  • The watcher is canceled before StopResourceAsync succeeds. If stopping is canceled or throws, ResourceCommandService returns a failed/canceled result while debugSessionActive remains true and no watcher remains to clear it when the browser later exits, leaving command state stale. Keep the watcher alive until stop succeeds, or restore/restart it on the failure path.
                // Cancel the watcher so it doesn't race with our state reset.
                await watcherCts.CancelAsync().ConfigureAwait(false);

                var dcpInstanceName = GetDcpInstanceName(debuggerResource);
                var orchestrator = context.ServiceProvider.GetRequiredService<ApplicationOrchestrator>();
                await orchestrator.StopResourceAsync(dcpInstanceName, context.CancellationToken).ConfigureAwait(false);

src/Aspire.Hosting.Blazor/Aspire.Hosting.Blazor.csproj:38

  • $(SharedDir) resolves to src/Shared, but neither BrowserDebuggerResource.cs nor BrowserLaunchConfiguration.cs exists there; both files remain under src/Aspire.Hosting.JavaScript. These compile items therefore reference missing source files and the project cannot build. Move the shared implementations into src/Shared (updating JavaScript too) or include an existing source path.
    <Compile Include="$(SharedDir)BrowserDebuggerResource.cs" Link="BrowserDebuggerResource.cs" />
    <Compile Include="$(SharedDir)BrowserLaunchConfiguration.cs" Link="BrowserLaunchConfiguration.cs" />

string? relativePath,
string debuggerBrowser = "msedge")
{
if (!builder.Configuration.GetValue<bool>(KnownConfigNames.WasmDebuggerEnabled, false))
annotation.OtlpPrefix = otlpPrefix;

EnsureEnvironmentCallback(host, annotation);
EnsureEnvironmentCallback(host, annotation, debuggerBrowser);
Comment on lines +33 to +35
<!-- StringComparers and KnownRelationshipTypes are provided by the referenced Aspire.Hosting
assembly (which source-shares them and exposes internals here via InternalsVisibleTo),
so they must not be source-shared again or they would conflict (CS0436). -->
Copilot AI review requested due to automatic review settings July 29, 2026 17:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 25 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (6)

src/Aspire.Hosting.Blazor/Aspire.Hosting.Blazor.csproj:35

  • Aspire.Hosting does not grant InternalsVisibleTo to Aspire.Hosting.Blazor (its friend list only includes JavaScript, Go, Python, tests, etc.). Removing the source-shared StringComparers therefore makes the existing use in BlazorHostedExtensions inaccessible, and the new helper also calls internal Hosting APIs such as TryGetInstances; the project will fail with CS0122 even after the missing source files are fixed. Add the required friend assembly or avoid those internal dependencies.
    <!-- StringComparers and KnownRelationshipTypes are provided by the referenced Aspire.Hosting
         assembly (which source-shares them and exposes internals here via InternalsVisibleTo),
         so they must not be source-shared again or they would conflict (CS0436). -->

src/Aspire.Hosting.Blazor/BlazorHostedExtensions.cs:83

  • Because IsInitialized returns here, debuggerBrowser from every proxy call after the first is silently ignored. For example, ProxyBlazorService(service) followed by ProxyBlazorTelemetry(debuggerBrowser: "chrome") still creates an Edge debugger despite the explicit Chrome argument. Make browser selection a once-per-host API, reject conflicting values, or store last-wins configuration and materialize the debugger from the final value.
        if (annotation.IsInitialized)
        {
            return;

src/Aspire.Hosting.Blazor/BlazorHostedExtensions.cs:185

  • Normalize the ProjectReference path before resolving it. On Linux, Path.Combine does not treat backslashes as separators, so the repository's own BlazorHosted.csproj reference (..\BlazorHosted.Client\...) fails File.Exists and silently falls back to the server project, preventing hosted WASM debugging. PathNormalizer.NormalizePathForCurrentPlatform already handles both separators.
                var fullPath = Path.GetFullPath(Path.Combine(serverDir, relPath!));

src/Aspire.Hosting.Blazor/BrowserDebuggerHelper.cs:110

  • This boolean check is not a concurrency guard. ResourceCommandService invokes command callbacks without serialization, so two requests can both observe false, start the debugger twice, and race while canceling/disposing/replacing watcherCts; start and stop can race similarly. Serialize both handlers with a shared gate or use an atomic state machine rather than relying on dashboard command visibility.
                if (debugSessionActive)
                {
                    return CommandResults.Success();

playground/BlazorHosted/BlazorHosted.Client/BlazorHosted.Client.csproj:21

  • The linked vscode-dotnettools#3002 was fixed and verified in C# 2.141.13/C# Dev Kit 3.14.215, then closed as completed on 2026-05-15 with guidance to use current stable extensions. Keeping this workaround now disables WebCIL for every build based on a resolved tooling regression; remove it or link an active hosted-mode issue that still reproduces with supported versions.
    <!-- Workaround: monovsdbg_wasm WebCIL parser fails in hosted mode.
         Serving plain .dll files bypasses the issue.
         Tracking: https://github.com/microsoft/vscode-dotnettools/issues/3002 -->
    <WasmEnableWebcil>false</WasmEnableWebcil>

src/Aspire.Hosting.Blazor/BrowserDebuggerHelper.cs:47

  • Concatenating two valid resource names plus this suffix does not guarantee a valid or unique derived name. Aspire resource names may each be 64 characters, so enabling debugging for longer valid gateway/app names makes AddResource throw because the debugger name exceeds 64 characters; delimiter ambiguity can also collide across different parent/child pairs. Generate a bounded, collision-resistant internal name instead.
        var debuggerResourceName = relativePath is not null
            ? $"{parentResource.Name}-{commandTarget.Resource.Name}-debugger"
            : $"{parentResource.Name}-wasm-debugger";

Comment on lines +37 to +38
<Compile Include="$(SharedDir)BrowserDebuggerResource.cs" Link="BrowserDebuggerResource.cs" />
<Compile Include="$(SharedDir)BrowserLaunchConfiguration.cs" Link="BrowserLaunchConfiguration.cs" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-integrations Issues pertaining to Aspire Integrations packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants