You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Support F# Hot Reload in CLI dotnet watch through a typed provider #55464
dotnet watch applies C# updates in place through Roslyn. F# projects always fall back to rebuild and restart. PR #55128, plus the matching dotnet/fsharp work (the #19941 train and #20030), shows in-place F# updates working end to end.
The prototype isn't the right final boundary though. It puts about 2,900 lines of F#-specific logic inside dotnet-watch: a project model, reflection-based FCS loading, session state, delta conversion, file classification. Most of that belongs behind the F# compiler service API.
This issue lays out the component split, the loading model, the v1 scope, and a delivery plan, following the three principles from the #55128 review feedback.
Contract
The provider contract is internal to dotnet-watch, implemented by a small SDK-side adapter over the typed FCS session API from #20030: FSharpChecker.CreateHotReloadSession and FSharpHotReloadSession (AddProject, EmitDelta, Commit, Discard, UpdateCapabilities, SetActiveStatements, Dispose). That API is the cross-repo boundary, and dotnet/fsharp owns it.
Nothing upstream works as a home for the contract. dotnet/fsharp can't reference the SDK without a dependency cycle. Roslyn's ExternalAccess.HotReload assembly isn't viable either: its API is internal, its product IVTs cover only Microsoft.DotNet.HotReload.Watch and Microsoft.DotNet.HotReload.Utils.Generator (a third grants test access), and it targets net10.0 while the official FCS targets netstandard2.0.
dotnet-watch is also moving to a new architecture: watch connects to a Roslyn LSP server and the two speak a small RPC hot reload protocol. That changes where the F# integration plugs in, but not the boundary. The natural F# shape there is a server that implements the same protocol on top of FSharpHotReloadSession. Watch-side integration work waits for that architecture to settle; the FCS session API stays the contract in every variant discussed so far.
Loading and versioning
dotnet-watch takes a compile-only reference to the pinned Microsoft.FSharp.Compiler package (PrivateAssets="all", ExcludeAssets="runtime;contentFiles"). FSharp.Compiler.Service and FSharp.Core don't ship in the watch tool layout. At startup, watch registers a resolver that loads both from the toolset directory sdk/<version>/FSharp/, in the default load context. This mirrors what watch already does for Roslyn: normal compile-time reference, layout exclusion, runtime resolution from Roslyn/bincore (dotnet-watch/Program.cs, RegisterAssemblyResolutionEvents).
The default load context matters. A separate load context gives the FCS types different identities and breaks a typed adapter.
The runtime assembly identities must match the compile-time FCS and FSharp.Core identities exactly. The session API is experimental, so no compatibility range is safe in either direction. When FscToolPath overrides the compiler, or the toolset FCS doesn't have the session API, watch keeps today's rebuild-and-restart behavior.
The prototype already resolved this directory through the DotnetFscCompilerPath MSBuild property, it just used reflection on top. The path derivation stays, the reflection goes away.
The prototype also aligned on-disk baseline builds with session compiles through a watch-set MSBuild property. Watch-specific build properties are the wrong mechanism, so that's dropped. The need is transitional anyway: once the in-process compile path lands (dotnet/fsharp#20031), the session produces the baseline and the deltas from the same compile, and no build property is involved. Any targets wiring for the compiler flag waits on the shipped compiler either way (stock fsc warns FS1063 on it today).
v1 scope
v1 applies in-place updates only when each running project and every transitive dependency is an F# project (.fsproj extension or the evaluated Language property; an unknown language counts as not F#). Call this the active F# graph.
Mixed active graphs keep today's behavior exactly: F# edits rebuild and restart, C# edits keep the unchanged Roslyn path. Unrelated non-F# projects elsewhere in the solution don't disable the feature, only the running projects and their dependencies matter. The cost is explicit: an F# app that references a C# library always restarts on edit in v1, even for body-only F# edits.
FSHRDL rude edit diagnostics go through the same watch console and browser channels as the ENC ones.
This keeps the new provider away from active C# graphs, which is the narrow scope the review feedback suggested.
Mixed graphs (v2)
Deferred, but recorded so v2 has a definition:
Metadata references are snapshots. A C# consumer compiles against the F# image captured when the reference was created, and an on-disk rebuild doesn't refresh it (verified with a repro on Roslyn 5.10: the error persists until the reference object itself is replaced). The reverse direction has the same problem.
A safe mixed policy needs a contract-change signal from each producing compiler. An empty metadata delta isn't a valid test: a body-only edit still produced a 404-byte metadata delta with 3 EncLog rows. F# can derive the signal from its symbol diffs. The watch-facing Roslyn API has no equivalent today.
Roslyn owns the single HotReloadMSBuildWorkspace/HotReloadService, so a unified cross-language graph is a Roslyn design change.
VS and VS Code
This issue only covers CLI dotnet watch, but the design shouldn't block the other hosts. The reusable core is the F# session in FCS: it has no dependency on watch, and it already models sequence point and active statement updates. The provider lifecycle mirrors the shared managed hot reload lifecycle (IManagedHotReloadLanguageService): start session, has changes, get updates, commit, discard, end session. The known gaps for VS and C# Dev Kit stay out of scope here: the brokered debugger contract, break states, debugger-side active statement tracking, richer update payloads, service discovery, and workspace integration.
Delivery plan
fsharp: land [main] Update dependencies from dotnet/aspnetcore #20030 and the remaining compiler slices. No new facade is needed, the session API is the contract. This work carries over to every integration variant above. The contract-change signal moves to v2.
sdk: wait for the new watch architecture, then build the typed integration in whatever shape it calls for (in-process adapter or protocol host), plus eligibility detection, F# project-reference reconstruction into graph-aware snapshots, the compiler flag wiring, and end-to-end tests. One transaction rule from the prototype must survive in any shape: refusing an F#-side restart must not call Roslyn DiscardUpdate() without a pending update. The prototype crashed watch exactly this way (InvalidOperationException: No pending update.).
PR #55128 is closed and preserved as the reference prototype.
Open questions
Active F# graph: must each running project and every transitive dependency be F#, so unrelated non-F# projects don't disable the feature?
Version policy: compile against the pinned Microsoft.FSharp.Compiler, require exact identity at runtime, and reject every custom FscToolPath?
🟨 Confidence:medium - Issue spans both Area-Watch and Area-FSharp; primary routing is Watch but F# is equally central.
➡️ Summary: This is a design proposal for a shared F# Hot Reload provider usable by dotnet-watch, Visual Studio, and VS Code. The team should review the proposed architecture before restructuring PR #55128.
changed the title [-]Design: F# Hot Reload provider shared by dotnet-watch, Visual Studio, and VS Code[/-][+]Support F# Hot Reload in CLI dotnet watch through a typed provider[/+]on Jul 27, 2026
Based on design feedback from @T-Gro (shared off-thread), I rewrote the issue body around three points.
The changes are:
The contract-home question is gone. The contract is internal to dotnet-watch. An SDK-side adapter uses FSharpChecker.CreateHotReloadSession and FSharpHotReloadSession from [main] Update dependencies from dotnet/aspnetcore #20030. The issue records the dependency cycle and the current Roslyn boundary constraints (internal API, product IVTs limited to two assemblies, net10.0 vs netstandard2.0).
The watch tool layout does not include FCS. I used watch's current Roslyn loading pattern. The adapter compiles against the pinned Microsoft.FSharp.Compiler package with PrivateAssets="all" and ExcludeAssets="runtime;contentFiles", and watch resolves FSharp.Compiler.Service and FSharp.Core together from sdk/<version>/FSharp/ in the default load context. The default context keeps type identities consistent for a typed adapter. Path derivation stays as in the prototype (DotnetFscCompilerPath); the reflection goes away.
v1 now requires an active F# graph: each running project and every transitive dependency must be F#. Mixed active graphs keep restart behavior, and unrelated C# projects elsewhere in the solution don't disable the feature. The issue states the cost (an F# app with a C# library always restarts in v1). The contract-change signal and the stale-reference analysis moved to a v2 section, with his note that a unified graph is a Roslyn design change.
Four things I want to confirm before I open the contract PR:
Is the active-F#-graph definition right? A solution-wide rule lets an unrelated C# test project disable the feature, which seems wrong.
Which internal contract shape do you prefer: a language-neutral provider interface with one F# adapter, or a direct F# integration point? The neutral interface can support another provider later but is slightly more watch surface.
Version policy: compile against the pinned Microsoft.FSharp.Compiler, require exact identity for the runtime-resolved FCS, and fall back to rebuild-and-restart for any custom FscToolPath. OK?
Must FSHRDL rude-edit diagnostics flow through the same watch console and browser channels as ENC diagnostics in v1?
@tmat, does the watch-side shape here work for you? The delivery plan starts with a contract-plus-tests PR that contains no F# code and leaves the Roslyn path untouched.
We are currently in the middle of rather large project system related architectural change across dotnet-watch, SDK, Roslyn, msbuild, VS and VS Code. I will keep F# on mind during these changes, but I'm afraid I won't have much bandwidth to spare for F# Hot Reload integration as much as I would love to get it working. Please, be patient, hopefully we can make a bit of progress every now and then.
The change in architecture will impact the way we can plug F# Hot Reload in, so it would be better to wait for it to get flushed out before we start any work on F#. Otherwise, it'd likely need to be reimplemented.
Comments and answers on the points listed above:
sdk: add /p:DotNetWatchBuild=true to watch-invoked builds. Independent fix, extracted from #55128.
I don't think we want to add any dotnet-watch specific property to the build. For a couple of reasons. First, if a project is already built, we don't want to rebuild it and trash the existing binaries if they already match the current source code. Second, this would make build different from build form other IDEs (VS, VS Code). That's the opposite of what we want. Today, we already have some properties like BuildingInsideVisualStudio that are set by the VS IDE. We consider these to be mistakes and want to remove them as much as possible.
What is the need for DotNetWatchBuild property from F# perspective?
Which internal contract shape do maintainers prefer: a language-neutral provider interface with one F# adapter, or a direct F# integration point?
Good question. The entry point we would define for F# would likely change with the new architecture. Currently, dotnet-watch loads MSBuild graph and uses it to retrieve the necessary project info, run design-time build evaluations and feed construction of Roslyn solution. Then we use the solution to determine differences and emit deltas. In the new design we are exploring (nothing is set in stone yet) dotnet-watch would connect to Roslyn LSP server and let it load and represent the graph directly. dotnet-watch would send updates from observed file changes to the server and the server would produce the EnC deltas. Hence, we would define RPC Hot Reload protocol between dotnet-watch and the server. We would want this protocol minimalistic, so that it's easy to reason about and version.
I can see a few of options for F# in that case. First, an F# LSP server could implement Hot Reload RPC protocol. That would work well for the V1 scope as you described it above. I.e. no mixed language graphs, or rather any change in a foreign language in a graph would be considered a rude edit, like we do today for F#. Second option (V2) would still be separate LSP servers (one for C#, one for F#) and some kind of handshake between them that would allow to resolve changes in foreign language projects. I haven't given any thought to how this protocol would work. Perhaps we could figure out some protocol that's not too complicated. Third option would be to load F# into Roslyn LSP server and define some kind of Roslyn API that plugs into ManagedHotReloadLanguageService. Also not sure yet how exactly would this work.
Must FSHRDL rude-edit diagnostics flow through the same watch console and browser channels as ENC diagnostics in v1?
I would think so. We want a good user experience, not something that feels "bolted-on".
Thanks @tmat, that all makes sense. I updated the issue body to reflect your reply, and closed the prototype PR for now.
On the DotNetWatchBuild question: the prototype used it to add --test:HotReloadDeltas to the on-disk baseline build, so the baseline and the in-memory session compiles agree on lowering. Without that, token mapping breaks when a delta is applied. It was always transitional. Once the in-process compile path lands (dotnet/fsharp#20031), the baseline and the deltas come from the same compile and no build property is needed.
The LSP direction sounds good from the F# side. Your first option is basically what the FCS work already supports. Two questions:
Where will the RPC protocol design happen? I'd like to follow along so it doesn't bake in C#-only assumptions.
Anything worth preparing on the F# side in the meantime, beyond landing the FCS session API?
Activity
github-actions commentedon Jul 24, 2026
🎯 Agentic Issue Triage
Triage Assessment:
@tmat|@dotnet/roslyn-idemedium- Issue spans both Area-Watch and Area-FSharp; primary routing is Watch but F# is equally central.➡️ Summary: This is a design proposal for a shared F# Hot Reload provider usable by dotnet-watch, Visual Studio, and VS Code. The team should review the proposed architecture before restructuring PR #55128.
[-]Design: F# Hot Reload provider shared by dotnet-watch, Visual Studio, and VS Code[/-][+]Support F# Hot Reload in CLI dotnet watch through a typed provider[/+]NatElkins commentedon Aug 14, 2026
Based on design feedback from @T-Gro (shared off-thread), I rewrote the issue body around three points.
The changes are:
The contract-home question is gone. The contract is internal to dotnet-watch. An SDK-side adapter uses
FSharpChecker.CreateHotReloadSessionandFSharpHotReloadSessionfrom [main] Update dependencies from dotnet/aspnetcore #20030. The issue records the dependency cycle and the current Roslyn boundary constraints (internal API, product IVTs limited to two assemblies, net10.0 vs netstandard2.0).The watch tool layout does not include FCS. I used watch's current Roslyn loading pattern. The adapter compiles against the pinned
Microsoft.FSharp.Compilerpackage withPrivateAssets="all"andExcludeAssets="runtime;contentFiles", and watch resolvesFSharp.Compiler.ServiceandFSharp.Coretogether fromsdk/<version>/FSharp/in the default load context. The default context keeps type identities consistent for a typed adapter. Path derivation stays as in the prototype (DotnetFscCompilerPath); the reflection goes away.v1 now requires an active F# graph: each running project and every transitive dependency must be F#. Mixed active graphs keep restart behavior, and unrelated C# projects elsewhere in the solution don't disable the feature. The issue states the cost (an F# app with a C# library always restarts in v1). The contract-change signal and the stale-reference analysis moved to a v2 section, with his note that a unified graph is a Roslyn design change.
Four things I want to confirm before I open the contract PR:
Microsoft.FSharp.Compiler, require exact identity for the runtime-resolved FCS, and fall back to rebuild-and-restart for any customFscToolPath. OK?@tmat, does the watch-side shape here work for you? The delivery plan starts with a contract-plus-tests PR that contains no F# code and leaves the Roslyn path untouched.
tmat commentedon Aug 15, 2026
Thanks for documenting the detailed design.
We are currently in the middle of rather large project system related architectural change across dotnet-watch, SDK, Roslyn, msbuild, VS and VS Code. I will keep F# on mind during these changes, but I'm afraid I won't have much bandwidth to spare for F# Hot Reload integration as much as I would love to get it working. Please, be patient, hopefully we can make a bit of progress every now and then.
The change in architecture will impact the way we can plug F# Hot Reload in, so it would be better to wait for it to get flushed out before we start any work on F#. Otherwise, it'd likely need to be reimplemented.
Comments and answers on the points listed above:
I don't think we want to add any dotnet-watch specific property to the build. For a couple of reasons. First, if a project is already built, we don't want to rebuild it and trash the existing binaries if they already match the current source code. Second, this would make build different from build form other IDEs (VS, VS Code). That's the opposite of what we want. Today, we already have some properties like
BuildingInsideVisualStudiothat are set by the VS IDE. We consider these to be mistakes and want to remove them as much as possible.What is the need for
DotNetWatchBuildproperty from F# perspective?Good question. The entry point we would define for F# would likely change with the new architecture. Currently, dotnet-watch loads MSBuild graph and uses it to retrieve the necessary project info, run design-time build evaluations and feed construction of Roslyn solution. Then we use the solution to determine differences and emit deltas. In the new design we are exploring (nothing is set in stone yet) dotnet-watch would connect to Roslyn LSP server and let it load and represent the graph directly. dotnet-watch would send updates from observed file changes to the server and the server would produce the EnC deltas. Hence, we would define RPC Hot Reload protocol between dotnet-watch and the server. We would want this protocol minimalistic, so that it's easy to reason about and version.
I can see a few of options for F# in that case. First, an F# LSP server could implement Hot Reload RPC protocol. That would work well for the V1 scope as you described it above. I.e. no mixed language graphs, or rather any change in a foreign language in a graph would be considered a rude edit, like we do today for F#. Second option (V2) would still be separate LSP servers (one for C#, one for F#) and some kind of handshake between them that would allow to resolve changes in foreign language projects. I haven't given any thought to how this protocol would work. Perhaps we could figure out some protocol that's not too complicated. Third option would be to load F# into Roslyn LSP server and define some kind of Roslyn API that plugs into ManagedHotReloadLanguageService. Also not sure yet how exactly would this work.
I would think so. We want a good user experience, not something that feels "bolted-on".
NatElkins commentedon Aug 17, 2026
Thanks @tmat, that all makes sense. I updated the issue body to reflect your reply, and closed the prototype PR for now.
On the DotNetWatchBuild question: the prototype used it to add
--test:HotReloadDeltasto the on-disk baseline build, so the baseline and the in-memory session compiles agree on lowering. Without that, token mapping breaks when a delta is applied. It was always transitional. Once the in-process compile path lands (dotnet/fsharp#20031), the baseline and the deltas come from the same compile and no build property is needed.The LSP direction sounds good from the F# side. Your first option is basically what the FCS work already supports. Two questions:
tmat commentedon Aug 17, 2026
So far, this is internal discussion. I'll keep F# on mind.
Too early to tell. I'll try to post updates here.