Skip to content

dotnet-watch: F# hot reload integration (inspection draft)#1

Draft
NatElkins wants to merge 705 commits into
mainfrom
fsharp-hotreload-watch-v2
Draft

dotnet-watch: F# hot reload integration (inspection draft)#1
NatElkins wants to merge 705 commits into
mainfrom
fsharp-hotreload-watch-v2

Conversation

@NatElkins

@NatElkins NatElkins commented Jun 12, 2026

Copy link
Copy Markdown
Owner

dotnet-watch: F# hot reload integration (inspection draft)

The dotnet-watch half of F# hot reload, paired with the compiler work in
NatElkins/fsharp#3 (full implementation).
Not for upstream until the FCS API ships publicly — this draft exists for review and for
anyone who wants to run the end-to-end experience.

🚀 Try it

hot-reload-quickstart.md
— two clones, two builds, copy-paste steps, ending with dotnet watch run hot-patching a
running F# console app (including adding lambdas to live methods). This branch builds the
complete .NET CLI (artifacts/bin/redist/Debug/dotnet) that the guide uses.

What this branch contains

  • FSharpHotReloadService + FSharpProjectInfo (src/Dotnet.Watch/Watch/HotReload/FSharp/):
    F# project discovery from the MSBuild graph, source/dependency change mapping, forced-build
    orchestration, and a reflection bridge into FSharp.Compiler.Service — no compile-time FCS
    dependency, graceful restart-fallback on stock toolsets
  • Session-object mode: one FSharpHotReloadSession per watch run (the FCS-side
    DebuggingSession analogue), every F# project added at prestart, per-project deltas with
    solution-wide commit. Multi-project interleaved edits apply in place — e2e
    ChangeFilesInFSharpAppAndLib_InterleavedEditsApplyInPlace edits a referenced library,
    then the app, then the library again, all without restart. Legacy single-project path
    preserved as the fallback for older FCS builds
    (DOTNET_WATCH_FSHARP_USE_SESSION_OBJECT=0 kill switch)
  • CompilationHandler integration: merged Roslyn + F# update flow; session-owned F# files
    kept out of the Roslyn workspace (prevents spurious ENC1009 restarts); runtime EnC
    capability forwarding with in-place refresh once agents connect (the session is prestarted
    before the app launches)
  • SupportsHotReload project capability in Microsoft.NET.Sdk.FSharp.targets (C#/VB parity)
  • MVID pinning at AddProject time: forced design-time rebuilds move the on-disk MVID;
    deltas must keep targeting the module the runtime actually loaded
  • Trace diagnostics throughout via DOTNET_WATCH_TRACE_FSHARP_HOTRELOAD=1

Evidence

19/19 F# watch test classes (FSharpHotReloadTests, FSharpHotReloadServiceTests,
FSharpReflectionHostTests) including the multi-project e2e, no-op edit handling (no
restart on whitespace saves), and dependency-edit handling — run against the locally built
FCS from hot-reload-v2.

Design docs

The architecture (session entity, snapshot contract, capability negotiation, determinism)
is documented on the compiler branch — start at
hot-reload-architecture.md.

@NatElkins

Copy link
Copy Markdown
Owner Author

Compiler-side counterpart is now under upstream review at dotnet/fsharp#19941. This branch gained: a one-variable master kill switch (DOTNET_WATCH_FSHARP_HOTRELOAD=0), the SupportsHotReload capability now advertised only under dotnet-watch builds (no VS blast radius), and session-object-only FCS support (the bridge no longer requires the retired legacy checker surface — e2e suite 8/0 against current FCS).

Comment thread test/dotnet-watch.Tests/HotReload/FSharpHotReloadTests.cs
NatElkins added a commit that referenced this pull request Jun 23, 2026
ChangeFileInFSharpProjectWithLoop_FirstEditAppliesInPlace asserted only the
applied/no-restart log messages, so a delta that "applies" without changing
behavior would still pass. Assert the running loop now prints the edited
string in place (mirrors ChangeFileInFSharpProjectWithLoop_AppliesOrRestarts).

Addresses review feedback on #1.
NatElkins added a commit that referenced this pull request Jun 23, 2026
…t-watch

The F# bridge collapsed the rude-edit reason to a record/DU ToString() dump and
logged it at Debug only, so an edit that forces a restart gave the user no reason.
Now that FSharpHotReloadError.UnsupportedEdit carries a structured rude-edit list
(Id + Severity + Message), read it via reflection (TryFormatRudeEdits) into a clean
"{Id}: {Message}" reason and report it as a warning instead of at Debug. The
extraction is defensive: any shape mismatch falls back to ToString(), so the bridge
stays correct against older FCS builds (where UnsupportedEdit still carries a string).

Pairs with the FCS-side structured diagnostics channel on dotnet/fsharp#19941.
Addresses review feedback on #1.
nagilson and others added 23 commits July 13, 2026 16:12
Co-authored-by: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jan Provaznik <janprovaznik@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…#54719)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com>
TestAsset.WithSource applied ~14 property/package-version substitutions by
calling UpdateProjProperty/ReplacePackageVersionVariable in a loop, each of
which reloaded and rewrote every project file in the asset. That meant ~14
XDocument load+save round-trips per project file during the setup of every
test that calls WithSource (the standard setup path for hundreds of SDK
integration tests).

Extract the per-document mutation bodies into private static helpers and apply
all substitutions in one WithProjectChanges pass, so each project file is
loaded and saved once. Behavior is unchanged: verified byte-identical output
across all 352 test-asset csproj files.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a22eead4-88ca-4142-a6d8-a7199db73289
WithSource copied every source file to the destination and then, in the
substitution pass, reloaded and re-saved each project/.xml file with the
target-framework, runtime-identifier, and package-version substitutions
applied. That meant every project file was written twice: once by File.Copy
and again by the substitution save that immediately overwrote it.

Skip the File.Copy for files that the substitution pass rewrites; instead load
those directly from the source and write the substituted result straight to the
destination once. Non-project files are still copied verbatim.

Because File.Copy is an exact byte copy, loading from the source is equivalent
to loading from the copied destination, so output is unchanged: verified
byte-identical across all 257 test assets (1439 files, 0 differences) while
eliminating 404 redundant writes (~22% of WithSource's file writes).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a22eead4-88ca-4142-a6d8-a7199db73289
Address review feedback: use OfType<XAttribute>() to filter out null
attributes returned by the LINQ chain instead of a redundant inner null-check.
Behavior is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a22eead4-88ca-4142-a6d8-a7199db73289
…55259)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…5218)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…s on x64 (dotnet#55205)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ovider.EnsureInstallPrerequisites (dotnet#55105)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
CODEOWNERS_PAT_0 through _2  would need to exist as secrets containing a token with exclusively organization Members: read and repository Issues: read for this to function.

This is scope creep so I will be likely removing this and deferring it to another PR but including here so we have it for future reference to build on top of.
JanProvaznik and others added 30 commits July 23, 2026 10:36
Copilot-Session: 1094e91b-1c9c-4704-998f-0a5959e900aa
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 366a002c-9ad2-4938-a79a-af9d2f425d24
dotnet#55388)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 76a86ef3-d73c-451c-8f6e-cef56bf3509c
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 72bf4a4e-15d3-4177-9246-c49ad3071b13
Resolve the TestFX dependency conflicts using main's newer coherent MSTest and Microsoft.Testing.Platform build.

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

Copilot-Session: 8bf6402b-2114-4555-97d4-9708f64b60da
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 99b4cd14-14dc-459c-bac7-f984c9d2632b
Co-authored-by: Chet Husk <chusk3@gmail.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 72bf4a4e-15d3-4177-9246-c49ad3071b13
Copilot-Session: 2331cf72-dd7c-4507-ba0f-792de68bd434
Update MSTest and Microsoft.Testing.Platform to TestFX build 324182, which includes microsoft/testfx#10169, and re-enable live capture in CI.

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

Copilot-Session: 8bf6402b-2114-4555-97d4-9708f64b60da
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ed opt-out property (dotnet#55447)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 72bf4a4e-15d3-4177-9246-c49ad3071b13
…#55117)

Co-authored-by: Jan Kratochvíl <kratochvil.jan@outlook.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 44aacce5-4a69-493c-989a-6bacbe193e08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.