[dotnet-watch] http transport for mobile#52581
Merged
jonathanpeppers merged 14 commits intodotnet:release/10.0.3xxfrom Feb 20, 2026
Merged
[dotnet-watch] http transport for mobile#52581jonathanpeppers merged 14 commits intodotnet:release/10.0.3xxfrom
jonathanpeppers merged 14 commits intodotnet:release/10.0.3xxfrom
Conversation
51 tasks
jonathanpeppers
added a commit
to jonathanpeppers/xamarin-android
that referenced
this pull request
Feb 6, 2026
Context: dotnet/sdk#52492 Context: dotnet/sdk#52581 `dotnet-watch` now runs Android applications via: dotnet watch 🚀 [helloandroid (net10.0-android)] Launched 'D:\src\xamarin-android\bin\Debug\dotnet\dotnet.exe' with arguments 'run --no-build -e DOTNET_WATCH=1 -e DOTNET_WATCH_ITERATION=1 -e DOTNET_MODIFIABLE_ASSEMBLIES=debug -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://localhost:9000 -e DOTNET_STARTUP_HOOKS=D:\src\xamarin-android\bin\Debug\dotnet\sdk\10.0.300-dev\DotnetTools\dotnet-watch\10.0.300-dev\tools\net10.0\any\hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll -bl': process id 3356 And so the pieces on Android for this to work are: ~~ Startup Hook Assembly ~~ Parse out the value: <_AndroidHotReloadAgentAssemblyPath>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_STARTUP_HOOKS')->'%(Value)'->Exists())</_AndroidHotReloadAgentAssemblyPath> And verify this assembly is included in the app: <ResolvedFileToPublish Include="$(_AndroidHotReloadAgentAssemblyPath)" /> Then, for Android, we need to patch up `$DOTNET_STARTUP_HOOKS` to be just the assembly name, not the full path: <_AndroidHotReloadAgentAssemblyName>$([System.IO.Path]::GetFileNameWithoutExtension('$(_AndroidHotReloadAgentAssemblyPath)'))</_AndroidHotReloadAgentAssemblyName> ... <RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(_AndroidHotReloadAgentAssemblyName)" /> ~~ Port Forwarding ~~ A new `_AndroidConfigureAdbReverse` target runs after deploying apps, that does: adb reverse tcp:9000 tcp:9000 I parsed the value out of: <_AndroidWebSocketEndpoint>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT')->'%(Value)')</_AndroidWebSocketEndpoint> <_AndroidWebSocketPort>$([System.Text.RegularExpressions.Regex]::Match('$(_AndroidWebSocketEndpoint)', ':(\d+)').Groups[1].Value)</_AndroidWebSocketPort> ~~ Prevent Startup Hooks in Microsoft.Android.Run ~~ When I was implementing this, I keep seeing *two* clients connect to `dotnet-watch` and I was pulling my hair to figure out why! Then I realized that `Microsoft.Android.Run` was also getting `$DOTNET_STARTUP_HOOKS`, and so we had a desktop process + mobile process both trying to connect! Easiest fix, is to disable startup hook support in `Microsoft.Android.Run`. I reviewed the code in `dotnet run`, and it doesn't seem correct to try to clear the env vars. ~~ Conclusion ~~ With these changes, everything is working! dotnet watch 🔥 C# and Razor changes applied in 23ms. This will depend on getting changes in dotnet/sdk before we merge.
jonathanpeppers
added a commit
to dotnet/android
that referenced
this pull request
Feb 6, 2026
Context: dotnet/sdk#52492 Context: dotnet/sdk#52581 `dotnet-watch` now runs Android applications via: dotnet watch 🚀 [helloandroid (net10.0-android)] Launched 'D:\src\xamarin-android\bin\Debug\dotnet\dotnet.exe' with arguments 'run --no-build -e DOTNET_WATCH=1 -e DOTNET_WATCH_ITERATION=1 -e DOTNET_MODIFIABLE_ASSEMBLIES=debug -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://localhost:9000 -e DOTNET_STARTUP_HOOKS=D:\src\xamarin-android\bin\Debug\dotnet\sdk\10.0.300-dev\DotnetTools\dotnet-watch\10.0.300-dev\tools\net10.0\any\hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll -bl': process id 3356 And so the pieces on Android for this to work are: ~~ Startup Hook Assembly ~~ Parse out the value: <_AndroidHotReloadAgentAssemblyPath>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_STARTUP_HOOKS')->'%(Value)'->Exists())</_AndroidHotReloadAgentAssemblyPath> And verify this assembly is included in the app: <ResolvedFileToPublish Include="$(_AndroidHotReloadAgentAssemblyPath)" /> Then, for Android, we need to patch up `$DOTNET_STARTUP_HOOKS` to be just the assembly name, not the full path: <_AndroidHotReloadAgentAssemblyName>$([System.IO.Path]::GetFileNameWithoutExtension('$(_AndroidHotReloadAgentAssemblyPath)'))</_AndroidHotReloadAgentAssemblyName> ... <RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(_AndroidHotReloadAgentAssemblyName)" /> ~~ Port Forwarding ~~ A new `_AndroidConfigureAdbReverse` target runs after deploying apps, that does: adb reverse tcp:9000 tcp:9000 I parsed the value out of: <_AndroidWebSocketEndpoint>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT')->'%(Value)')</_AndroidWebSocketEndpoint> <_AndroidWebSocketPort>$([System.Text.RegularExpressions.Regex]::Match('$(_AndroidWebSocketEndpoint)', ':(\d+)').Groups[1].Value)</_AndroidWebSocketPort> ~~ Prevent Startup Hooks in Microsoft.Android.Run ~~ When I was implementing this, I keep seeing *two* clients connect to `dotnet-watch` and I was pulling my hair to figure out why! Then I realized that `Microsoft.Android.Run` was also getting `$DOTNET_STARTUP_HOOKS`, and so we had a desktop process + mobile process both trying to connect! Easiest fix, is to disable startup hook support in `Microsoft.Android.Run`. I reviewed the code in `dotnet run`, and it doesn't seem correct to try to clear the env vars. ~~ Conclusion ~~ With these changes, everything is working! dotnet watch 🔥 C# and Razor changes applied in 23ms. This will depend on getting changes in dotnet/sdk before we merge.
rolfbjarne
reviewed
Feb 11, 2026
jonathanpeppers
commented
Feb 11, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a WebSocket-based hot reload transport to dotnet watch to support mobile scenarios (Android/iOS) where named pipes aren’t viable, selecting the transport via a new HotReloadWebSockets project capability.
Changes:
- Add WebSocket transport selection for hot reload (capability detection + mobile app model) and plumb a configurable port via environment options.
- Implement WebSocket server/client plumbing for hot reload (watch side Kestrel WebSocket server + agent-side WebSocket transport) while reusing the existing binary delta protocol.
- Add a new test project + test coverage validating WebSocket transport selection and hot reload behavior.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/dotnet-watch.Tests/HotReload/MobileHotReloadTests.cs | Adds end-to-end test verifying WebSocket hot reload path is selected and works. |
| test/TestAssets/TestProjects/WatchMobileApp/WatchMobileApp.csproj | New test asset project that opts into WebSocket transport via HotReloadWebSockets capability. |
| test/TestAssets/TestProjects/WatchMobileApp/Program.cs | New test app used by dotnet watch tests to validate hot reload output changes. |
| test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs | Updates agent/client tests to use the new transport abstraction and listener. |
| src/BuiltInTools/Watch/UI/IReporter.cs | Adds a new debug descriptor to log “Application kind: WebSockets.” |
| src/BuiltInTools/Watch/Context/EnvironmentVariables.cs | Adds env var names + parsing for the hot reload HTTP port override and WebSocket endpoint name. |
| src/BuiltInTools/Watch/Context/EnvironmentOptions.cs | Adds HotReloadHttpPort option and wires it to environment variable reading. |
| src/BuiltInTools/Watch/Build/ProjectGraphUtilities.cs | Adds capability-based detection for WebSocket hot reload projects. |
| src/BuiltInTools/Watch/Build/BuildNames.cs | Introduces ProjectCapability.HotReloadWebSockets constant. |
| src/BuiltInTools/Watch/AppModels/MobileAppModel.cs | New app model that creates a MobileHotReloadClient (WebSocket-based). |
| src/BuiltInTools/Watch/AppModels/HotReloadAppModel.cs | Selects the mobile/WebSocket app model when capability is present. |
| src/BuiltInTools/HotReloadClient/Web/KestrelWebSocketServer.cs | New Kestrel-hosted WebSocket server base used by mobile hot reload. |
| src/BuiltInTools/HotReloadClient/MobileHotReloadClient.cs | Implements watch-side WebSocket hot reload client + server coordination. |
| src/BuiltInTools/HotReloadAgent.Host/Transport.cs | Introduces a transport abstraction for agent communications. |
| src/BuiltInTools/HotReloadAgent.Host/NamedPipeTransport.cs | Moves named-pipe specifics behind the new Transport abstraction. |
| src/BuiltInTools/HotReloadAgent.Host/WebSocketTransport.cs | Adds WebSocket transport implementation for the agent startup hook. |
| src/BuiltInTools/HotReloadAgent.Host/Listener.cs | Refactors the listener to work over the new transport abstraction (pipe or WebSocket). |
| src/BuiltInTools/HotReloadAgent.Host/StartupHook.cs | Switches startup hook connection logic to choose transport from env vars. |
| src/BuiltInTools/HotReloadAgent.Data/AgentEnvironmentVariables.cs | Adds DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT constant. |
| documentation/specs/dotnet-watch-for-maui.md | Adds a spec describing capability detection, env vars, and workload integration points. |
test/Microsoft.Extensions.DotNetDeltaApplier.Tests/HotReloadClientTests.cs
Show resolved
Hide resolved
simonrozsival
approved these changes
Feb 12, 2026
This was referenced Feb 12, 2026
Open
This was using a HTTP listener at first, later switched to WebSockets.
9001e8b to
41e4074
Compare
* Now uses `dotnet run -e` environment variables for everything * Now uses web sockets * Adds tests for mobile hot reload
Introduce RSA-based authentication for mobile hot reload WebSocket connections. Adds DOTNET_WATCH_HOTRELOAD_WEBSOCKET_KEY env var and constant, and propagates the server public key to the client. WebSocketTransport now accepts a server public key, encrypts a random 32-byte secret with RSA-OAEP-SHA256, and sends it as a URL-safe Base64 subprotocol token. HotReloadWebSocketServer uses SharedSecretProvider to expose the public key and to decrypt/validate the incoming secret, rejecting connections with missing/invalid secrets. Also adds a Base64Url helper for URL-safe Base64 encoding/decoding and updates documentation and cleanup to dispose the shared secret provider.
41e4074 to
9af2d80
Compare
…ientTransport` abstraction Addressing: dotnet#52581 (comment)
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 13, 2026
tmat
reviewed
Feb 14, 2026
tmat
reviewed
Feb 14, 2026
…er into WebSocketClientTransport - Seal KestrelWebSocketServer: remove inheritance, use RequestDelegate, change useTls to int? securePort, make ConvertToWebSocketUrl static - Delete AgentWebSocketServer: move connection handling and shared secret validation into WebSocketClientTransport directly - Unify agent protocol: always write ResponseType byte prefix including for InitializationResponse (both NamedPipe and WebSocket) - Change ClientTransport.WaitForConnectionAsync to return Task (not Task<string>), read init response via ReadAsync in DefaultHotReloadClient - Refactor NamedPipeClientTransport: create pipe in constructor (non-nullable field), remove Debug.Assert checks - Add AgentWebSocketSecurePort environment variable and option - Enable static asset updates for mobile (enableStaticAssetUpdates: true) - Use pattern matching in Transport.cs (uri.Scheme is not) - Use WaitUntilOutputContains in MobileHotReloadTests for async assertions
This will throw if anything is invalid, we don't need to use the result.
…ncoding Align mobile WebSocket auth with BrowserRefreshServer by using WebUtility.UrlEncode/UrlDecode instead of custom Base64Url encoding. Delete the now-unused Base64Url helper class.
tmat
reviewed
Feb 20, 2026
tmat
reviewed
Feb 20, 2026
tmat
reviewed
Feb 20, 2026
Member
|
Finished another pass. Just a few minor comments. |
- WebSocketClientTransport: replace blocking EnsureServerStarted() with async static factory method CreateAsync(). Constructor is now private. - WebSocketClientTransport: invert subprotocol null check to unnest the try-catch block in HandleRequestAsync. - KestrelWebSocketServer: add GetWebSocketUrl(url, hostName?) shared helper that both BrowserRefreshServer and WebSocketClientTransport use for HTTP-to-WebSocket URL translation (127.0.0.1 -> localhost when no custom hostname is set). Renamed from ConvertToWebSocketUrl. - BrowserRefreshServer: GetServerUrls now delegates to the shared helper. - MobileAppModel: updated to await the new async factory. - Test regex updated to match ws://localhost.
tmat
approved these changes
Feb 20, 2026
Member
|
/ba-g Linux CI leg is broken. |
888e971
into
dotnet:release/10.0.3xx
26 of 28 checks passed
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.
Context: #52492
dotnet watchfor .NET MAUI ScenariosOverview
This implements
dotnet watch/ Hot Reload for mobile platforms (Android, iOS), which cannot use the standard named pipe transport. Similar to how web applications already use websockets for reloading CSS and JavaScript, we use the same model for mobile applications.Transport Selection
adb reversetunnels the connectiondotnet-watchdetects WebSocket transport via theHotReloadWebSocketscapability:Mobile workloads (Android, iOS) add this capability to their SDK targets. This allows any workload to opt into WebSocket-based hot reload.
SDK Changes
Architecture
Both named pipe and WebSocket transports now share a common
ClientTransportabstraction consumed byDefaultHotReloadClient:NamedPipeClientTransport— existing local IPC pathWebSocketClientTransport— new path for mobile, composes a sealedKestrelWebSocketServerThe agent side mirrors this with a
Transportbase class (NamedPipeTransport/WebSocketTransport).WebSocket Details
dotnet-watchalready has a WebSocket server for web apps:BrowserRefreshServer. This server:http://127.0.0.1:<port>orhttps://localhost:<port>)aspnetcore-browser-refresh.js) injected into web pagesFor mobile, we reuse the Kestrel infrastructure but with a different protocol:
BrowserRefreshServerWebSocketClientTransportWebSocketClientTransportcomposes a sealedKestrelWebSocketServer(shared withBrowserRefreshServer) and speaks the same binary protocol as the named pipe transport, just over WebSocket instead. Static asset updates (CSS) are also supported.WebSocket Authentication
To prevent unauthorized processes from connecting to the hot reload server,
WebSocketClientTransportuses RSA-based authentication identical toBrowserRefreshServer:SharedSecretProvidercreates a 2048-bit RSA key on startupDOTNET_WATCH_HOTRELOAD_WEBSOCKET_KEYWebUtility.UrlEncode) and sent as the WebSocket subprotocol header — same encoding asBrowserRefreshServerWebSocketClientTransport.HandleRequestAsyncdecodes and decrypts the subprotocol value, accepting the connection only if decryption succeedsHTTPS Support
KestrelWebSocketServersupports binding both HTTP and HTTPS ports simultaneously via thesecurePortparameter. WhenAgentWebSocketSecurePortis configured, the server binds a WSS endpoint alongside the WS endpoint.Environment Variables
dotnet-watchlaunches the app via:The port is dynamically assigned (defaults to 0, meaning the OS picks an available port) to avoid conflicts in CI and parallel test scenarios. The
DOTNET_WATCH_AGENT_WEBSOCKET_PORTenvironment variable can override this if a specific port is needed.DOTNET_WATCH_AGENT_WEBSOCKET_SECURE_PORToptionally enables WSS.These environment variables are passed as
@(RuntimeEnvironmentVariable)MSBuild items to the workload. Seedotnet-run-for-maui.mdfor details ondotnet runand environment variables.Android Workload Changes (Example Integration)
dotnet/android#10770 — RuntimeEnvironmentVariable Support
Enables the Android workload to receive env vars from
dotnet run -e:<ProjectCapability Include="RuntimeEnvironmentVariableSupport" /><ProjectCapability Include="HotReloadWebSockets" />to opt into WebSocket-based hot reload@(RuntimeEnvironmentVariable)items, so they will apply to Android.dotnet/android#10778 — dotnet-watch Integration
DOTNET_STARTUP_HOOKS, includes the assembly in the app package, rewrites the path to just the assembly name (since the full path doesn't exist on device)adb reverse tcp:<port> tcp:<port>so the device can reach the host's WebSocket server via127.0.0.1:<port>(port is parsed from the endpoint URL)Microsoft.Android.Run(the desktop launcher) so only the mobile app connectsData Flow
dotnet-watchbuilds the project, detectsHotReloadWebSocketscapabilitydotnet run -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://127.0.0.1:<port> -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_KEY=<key> -e DOTNET_STARTUP_HOOKS=...Transport.TryCreate()reads env vars →WebSocketTransportencrypts secret with RSA public key → connects tows://127.0.0.1:<port>with encrypted secret as subprotocolWebSocketClientTransportvalidates the encrypted secret, accepts connectioniOS
Similar changes will be made in the iOS workload to opt into WebSocket-based hot reload:
<ProjectCapability Include="HotReloadWebSockets" />Dependencies
$DOTNET_STARTUP_HOOKS— needed for Mono runtime to honor startup hooks (temporary workaround viaRuntimeHostConfigurationOption)