[Combined review] Simplify browser tools to the provider contract - #56118
Draft
javiercn wants to merge 19 commits into
Draft
[Combined review] Simplify browser tools to the provider contract#56118javiercn wants to merge 19 commits into
javiercn wants to merge 19 commits into
Conversation
Move standalone net11 Blazor WASM activation to a watch-owned provider reached through Blazor Gateway's existing reverse proxy. Behavior: changed: standalone net11 WASM discovers provider-owned browser tooling without BrowserRefresh startup injection State: builds; regression coverage follows in the next commit Review hint: focus on provider discovery, generation replay, and the fixed Gateway route; the remainder is wiring Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49a13c60-1d66-4321-bfdb-01275ead89d6
Add provider endpoint, replay-store, launch-selection, and build-only WASM asset coverage for the Gateway-native implementation. Behavior: preserved State: complete for PR #56071 Review hint: generated Static Web Assets baseline additions represent the two build-only hot reload modules Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49a13c60-1d66-4321-bfdb-01275ead89d6
Use the built-in BodyTagHelper component pipeline to append one external browser-tools script for supported net11 MVC and Razor Pages responses. Behavior: changed: supported MVC and Razor Pages activate browser tooling without generic HTML rewriting State: builds; regression coverage follows in the next commit Review hint: the supported boundary is the TagHelper registration and body-only PostContent append; static HTML remains user-activated Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49a13c60-1d66-4321-bfdb-01275ead89d6
Cover body-only script insertion, hosting-startup registration, legacy fallback, and app-model selection for the MVC/Razor Pages boundary. Behavior: preserved State: complete for PR #56072 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49a13c60-1d66-4321-bfdb-01275ead89d6
Move net11 server-hosted browser tooling onto the watch-owned protocol through a fixed loopback HTTP/WebSocket forwarder, including hosted WASM when both sides target net11. Behavior: changed: modern server-hosted applications share provider-owned scripts, state, replay, and connection protocol State: builds; regression coverage follows in the next commit Review hint: verify the fixed destination/route, WebSocket subprotocol forwarding, and explicit TFM selection; legacy hosting startup remains isolated Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49a13c60-1d66-4321-bfdb-01275ead89d6
Cover HTTP and WebSocket forwarding, provider-hosted resources, reconnect behavior, and explicit net8/net9/net10/net11 and mixed hosted-WASM launch strategies. Behavior: preserved State: complete for modern forwarding and compatibility selection Review hint: the net8-net10 and mixed hosted-WASM rows prove that only all-net11 hosted applications select the provider path Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49a13c60-1d66-4321-bfdb-01275ead89d6
Stop publishing the provider-hosted client as an application static asset, remove the obsolete legacy-injection switch and test-only launch facade, and reduce launch features to the managed-hot-reload decision. Behavior: preserved: required pre-net11 and mixed hosted-WASM legacy paths remain selected and functional State: complete Review hint: the large baseline deletions are the removed static client asset; the client source remains embedded in and served by dotnet watch Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49a13c60-1d66-4321-bfdb-01275ead89d6
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49a13c60-1d66-4321-bfdb-01275ead89d6
Remove the legacy response-rewriting and application-hosted browser tooling paths, and use provider forwarding and replay across supported target frameworks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remove the unreachable return after the provider-only reconnect loop. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
javiercn
commented
Sep 3, 2026
| ? HotReloadAppModel.InferFromProject(context, projectRootNode) as WebApplicationAppModel | ||
| : null; | ||
| var browserRefreshServer = webAppModel != null | ||
| ? await context.BrowserRefreshServerFactory.GetOrCreateBrowserRefreshServerAsync(projectRootNode!, webAppModel, shutdownCancellationToken) |
Member
Author
There was a problem hiding this comment.
Could we pass in the YARP config and the HOSTING_STARTUP unconditionally? Only one will be active anyways and that way we don't have to infer anything.
Reduce the diff against main without changing the intended provider-only behavior (standalone WASM via the devserver Gateway, server-hosted apps via forwarding): - Revert DotNetWatchBuild propagation; reserve only DotNetWatchBrowserTools. - Revert the UsingBrowserRefreshMiddleware -> UsingBrowserTools message rename. - Replace the 3-file IBrowserToolsLaunchConfigurator abstraction with a virtual WebApplicationAppModel.ConfigureBrowserToolsLaunchEnvironment method. - Drop BrowserToolsForwarderOptions, IBrowserToolsUpdateStore, the unused browser-tools sessionId, and the duplicate JsonDelta transport DTO. - Restore WebServerHost/KestrelWebSocketServer/app models close to main and undo formatting-only wrapping. - Rebase the browser client on main's WebSocketScriptInjection.js as a git rename, preserving the toast UI, glyphs, sentinel and console messages, and restore the agent JS module and InitializeAsync(baseUri) export. - Add a WasmSdk watch activation initializer for every supported TFM so net8/ net9 standalone WASM still activates browser tools after legacy response injection was removed; net10+ keeps the Hot Reload agent path. Remove the Web SDK initializer in favor of the existing TagHelper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Restore the Web SDK build-only JS initializer and its focused integration test. BrowserRefreshTagHelperComponent only runs for MVC/Razor TagHelper processing, so a Blazor Web App using static SSR or Interactive Server without a WebAssembly client had no activation path. Deduplication stays in the browser client. Make the agent initializer read the dotnet-watch activation flag through a function instead of capturing it at module evaluation, because library initializer modules can be evaluated in any order. Start the browser-tools bootstrap from the WebAssembly activation initializer's onRuntimeReady hook instead of a top-level fire-and-forget import, so the client connects after the runtime is up. Make waitForHotReloadApply wait while neither apply function exists, including while window.Blazor is absent, and report failure instead of acknowledging replayed or live updates that could not be applied. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4e3d4dfa-da6c-410a-9cbe-d09d896af75b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 73e5ae8a-3258-40da-9a27-a1fb7a8feffe
- DotNetWatcher/HotReloadClients go back to main's `browserRefreshServer?.ConfigureLaunchEnvironment(environmentBuilder)`. The app-model-specific configuration is passed to the refresh server as a callback when the server is created, so both the hot reload and the `--no-hot-reload` watch loops configure the launch environment exactly as they do in main. - The WASM watch initializer and the Hot Reload agent initializer no longer communicate through `globalThis`. The watch initializer writes the runtime configuration variables the agent already looks for from `onRuntimeConfigLoaded`; the agent reads them from `onRuntimeReady`. Blazor runs every `onRuntimeConfigLoaded` before any `onRuntimeReady` and shares one config object, so this is ordered without relying on module load order. A cross-module import is not viable: the agent module only exists on .NET 10+, the watch module only exists under watch, and both URLs are fingerprinted. - The browser client keeps its `WebSocketScriptInjection.js` name so git recognizes the move into the shared HotReload client project. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4e3d4dfa-da6c-410a-9cbe-d09d896af75b
Standalone WebAssembly no longer needs its own Gateway/YARP launch configuration (the ReverseProxy route returns 404 on every host), so `ConfigureLaunchEnvironment` has a single implementation again. Restore main's shape instead of routing it through the app model: - `AbstractBrowserRefreshServer` takes `middlewareAssemblyPath` again and configures the forwarding environment itself; the callback parameter and `WebApplicationAppModel.ConfigureBrowserToolsLaunchEnvironment` are gone. - `BlazorWebAssemblyAppModel` is now identical to main. - `BrowserRefreshServerTests`, deleted earlier on this branch, is restored and covers the provider launch environment; the project-graph-based `BlazorWebAssemblyAppModelTests` it superseded is removed. Both test mocks go back to main's constructor shape. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4e3d4dfa-da6c-410a-9cbe-d09d896af75b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 73e5ae8a-3258-40da-9a27-a1fb7a8feffe
Executable browser tools JavaScript is now served only by the application build output. The provider being authenticated no longer serves any code, so the build pinned public key it is authenticated with is meaningful. dotnet watch creates one RSA key pair per invocation before any project is built and flows the public half through the reserved MSBuild property DotNetWatchBrowserToolsPublicKey. A shared Static Web Assets targets file generates a build only, publish excluded configuration module under obj that pins that public key and the fixed provider route, and imports the separate application hosted client module. The private key never leaves the watch process and the secret the browser generates is never persisted. Replay moves into the authenticated WebSocket handshake: the provider sends the current snapshot first and releases queued live messages only after the browser acknowledges it, per connection and in parallel across connections. That makes session.json, protocol version negotiation, the HTTP updates endpoint and the wire level generation id unnecessary, so all of them are removed along with the provider hosted client script. Also restores the Blazor Gateway reverse proxy route for standalone WebAssembly, which cannot use ASP.NET Core hosting startups. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ca6d4419-8728-4fbe-8392-fae213d6efa0
…hot reload Three adjudication-backed corrections on top of the browser-tools session redesign. BrowserToolsForwarder turned the provider's deliberate pre-upgrade HTTP 400 for an invalid encrypted WebSocket subprotocol into an app-origin 502, hiding the rejection contract from the browser. It now opts into CollectHttpResponseDetails and relays the provider's status when the upgrade failed with an error status, matching what ForwardHttpAsync already does for plain HTTP. A status recorded after the provider switched protocols (101) and an absent status both stay 502, so a genuine transport failure is never reported as an authentication failure. Both APIs are .NET 7+ while the assembly targets net6.0, so they are read through cached reflection. .NET 9 standalone WebAssembly authenticated and reported successful delta delivery, but no edit took effect. Its WebAssemblyHotReload creates the hot reload agent only inside InitializeAsync, which the runtime runs only when __ASPNETCORE_BROWSER_TOOLS is set; without it applyHotReloadDeltas is a silent no-op. The WebAssembly activation initializer became a checked-in template whose gate the SDK substitutes for target framework version 9.0 alone: .NET 8 must not get the variable because it would import the removed blazor-hotreload.js module, and .NET 10+ ships its own agent. That initialization path also probes /_framework/blazor-hotreload for previously applied deltas. The redesign removed the endpoint, so the SPA fallback answered with HTML and the runtime failed to parse it. The injected middleware now answers the route locally with an empty update array. It is never forwarded to the provider and never carries deltas: replay belongs to the authenticated WebSocket, and serving updates over an unauthenticated route would be a security regression. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ca6d4419-8728-4fbe-8392-fae213d6efa0
dotnet watch could report "C# and Razor changes applied" while nothing was applied. WebAssemblyHotReload.ApplyHotReloadDeltas returned an empty log when the managed agent was null, the browser client turned that into a successful acknowledgement, and watch reports an acknowledged update as applied. - Record why the agent was not created and throw that reason from ApplyHotReloadDeltas instead of returning an empty log. - Suppress the applied message and the Aspire notification when any client's apply task failed, so the console and the notification channel agree. - Report a replay apply failure as an error without failing initialization: the provider disposes a connection whose initialization failed and the client reloads on close, which would loop. - Create window.Blazor._internal unconditionally in the agent initializer; the nested guard threw when Blazor existed without _internal. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ca6d4419-8728-4fbe-8392-fae213d6efa0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
REVIEW ONLY — DO NOT MERGE
This OPEN DRAFT pull request is a combined review view plus an experimental browser-tools redesign. It is not an authoritative merge unit and must not be merged. Do not enable auto-merge.
The authoritative native stack remains #56076, with merge layers #56071, #56072, and #56080. This review branch is intentionally distinct from that stack.
The tree through
73667f3d1f086479f5d8babb52c7b3ed015f1253matched the final authoritative stack head. The current review tree adds the experimental provider-only simplification, minimization, and validated session/key redesign.Final experimental design
/connectand/clear-cache./session.json,/updates/{generation}.json,protocolVersion, and wire-level generation/update IDs are removed.ReverseProxy__*route and the inherited provider/hosting-startup forwarding environment. Gateway consumes the route; historicalblazor-devserverconsumes the forwarder.Authoritative validation
Validated exact head
e75615ca747c077ad14f1e7a6c0db5a10b0683b2after deleting the exact disposablebin/objdirectories between candidate SDK transitions:/connect101, observable C#/CSS updates;remainingPids: []; final matching process count: 0.Every strengthened active case verified trusted app-hosted assets, a valid RSA-2048 key, authenticated app-origin
/connect101, watch-observed browser connection, missing/invalid subprotocol rejection with HTTP 400, removed endpoint 404s, zero executable resources loaded from the provider prefix, observable C# and CSS updates without host restart, and nonempty apply capabilities. Net9 alone enabled its compatibility flag and received the exact local[]replay response.The prior apparent net10/net11 apply failure was reproduced and traced to stale downstream runtime assets retained in disposable app
bin/obj; clean rebuilds fixed it. That condition also exposed and motivated the genuine false-success hardening described above.The first full-matrix pass was 33/34 because the external synthetic net11-server/net10-client generator combined a net10 standalone import-map/fingerprint assumption with legacy
UseStaticFiles. Only disposable external assets/generator logic were corrected; no product source changed. Focused retest and the clean full rerun passed 34/34.Evidence root (external to the repository):
C:\Users\jacalvar\.copilot\session-state\72526d74-a3b0-4f9c-8057-e6902d92f75c\files\standalone-adjudication\candidate-e75615ca74Key artifacts:
results\authoritative-summary.jsonresults\final-results.jsonresults\standalone-summary.jsonresults\gateway-abc-summary.jsonresults\clean-actions.jsonresults\mixed-110-100-clean-actions.jsonresults\asset-provenance.jsonresults\browser; logs underlogsPlease use this PR only to review the aggregate experimental design. Do not merge it.