Blazor hosting changes for WebAssembly debugging - #18928
Conversation
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)
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 18928Or
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>
There was a problem hiding this comment.
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.
AddResourcevalidates the generated name and will reject, for example, two 28-character names plus this suffix. Apply the establishedNameValidationPolicyAnnotation.Nonepolicy 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_rootis copied directly to the VS Codepwa-*debug configuration'swebRoot(extension/src/debugger/languages/browser.ts:24-30), which requires a directory. Passing the.csprojfile path makes source-map resolution root beneath a file and prevents the debugger from locating sources. Use the already computedclientProjectDirhere and update the assertions accordingly.
WebRoot = clientProjectPath,
src/Aspire.Hosting.Blazor/BrowserDebuggerHelper.cs:111
- This check does not serialize command execution.
ResourceCommandServiceinvokes callbacks without a per-resource lock, so two concurrent requests can both observefalse, start the debugger twice, and concurrently cancel/dispose/reassignwatcherCts; 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
StopResourceAsyncsucceeds. If stopping is canceled or throws,ResourceCommandServicereturns a failed/canceled result whiledebugSessionActiveremains 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 tosrc/Shared, but neitherBrowserDebuggerResource.csnorBrowserLaunchConfiguration.csexists there; both files remain undersrc/Aspire.Hosting.JavaScript. These compile items therefore reference missing source files and the project cannot build. Move the shared implementations intosrc/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); |
| <!-- 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). --> |
There was a problem hiding this comment.
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.Hostingdoes not grantInternalsVisibleTotoAspire.Hosting.Blazor(its friend list only includes JavaScript, Go, Python, tests, etc.). Removing the source-sharedStringComparerstherefore makes the existing use inBlazorHostedExtensionsinaccessible, and the new helper also calls internal Hosting APIs such asTryGetInstances; 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
IsInitializedreturns here,debuggerBrowserfrom every proxy call after the first is silently ignored. For example,ProxyBlazorService(service)followed byProxyBlazorTelemetry(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
ProjectReferencepath before resolving it. On Linux,Path.Combinedoes not treat backslashes as separators, so the repository's ownBlazorHosted.csprojreference (..\BlazorHosted.Client\...) failsFile.Existsand silently falls back to the server project, preventing hosted WASM debugging.PathNormalizer.NormalizePathForCurrentPlatformalready 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.
ResourceCommandServiceinvokes command callbacks without serialization, so two requests can both observefalse, start the debugger twice, and race while canceling/disposing/replacingwatcherCts; 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
AddResourcethrow 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";
| <Compile Include="$(SharedDir)BrowserDebuggerResource.cs" Link="BrowserDebuggerResource.cs" /> | ||
| <Compile Include="$(SharedDir)BrowserLaunchConfiguration.cs" Link="BrowserLaunchConfiguration.cs" /> |
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:BrowserDebuggerHelperthat 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 optionaldebuggerBrowserparameter (defaults to"msedge", also supports"chrome") that is passed through to the WASM app registration, inspired by the JavaScript app debugger support.BlazorHostedExtensionsresolves 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.BrowserDebuggerHelper,BlazorHostedExtensions, and theWithBlazorAppflows.Checklist
<remarks />and<code />elements on your triple slash comments?