Every .NET developer who has tried hot reload with an Aspire-orchestrated application has hit the same wall. You change a line in a Razor component, save the file, and instead of seeing the change appear in your browser, dotnet watch tears down the entire AppHost, rebuilds everything, and relaunches every service. A two-second feedback loop becomes a thirty-second one. It was bad enough that many teams simply stopped using dotnet watch with Aspire and fell back to manual restarts.

The root cause was architectural. When Aspire launched child services, it passed --no-hot-reload --non-interactive to each process, disabling hot reload entirely. The AppHost itself was the watched process, so any file change in any project triggered a full restart of the orchestrator — and everything it managed. This was not a bug anyone could work around at the application level; it required a fundamental change in how dotnet watch and Aspire communicate.

.NET 11 Preview 3 ships that change. The SDK now includes a purpose-built coordination layer between dotnet watch and the Aspire application model, plus two quality-of-life improvements: crash recovery with deferred relaunch and proper Ctrl+C handling for Windows desktop applications.

The Aspire integration architecture

The new integration introduces a tool called Watch.Aspire that sits between dotnet watch and the Aspire AppHost. It uses a three-command architecture — host, server, and resource — coordinated through named pipes.

Here is how the pieces fit together:

The process hierarchy looks like this:

CLI (launches directly)
 ├── DCP (persistent, monitors CLI PID)
 │   ├── Dashboard (persistent, gRPC)
 │   └── dotnet watch (restartable wrapper)
 │       └── AppHost (detects CLI-owned mode via DCP_KUBECONFIG_PATH)
 └── User resources (api, worker, frontend, etc.)

The critical design decision is that DCP (the Distributed Control Plane) and the dashboard remain persistent across AppHost restarts. When the AppHost needs to restart — say, because you changed its Program.cs — only the AppHost itself is torn down and relaunched. Your Redis container, your database, and your API service keep running. The dashboard retains its state. This is what makes the experience feel fast: the expensive infrastructure stays warm.

Named pipes: the coordination protocol

Communication between the server and the AppHost happens over two named pipes per resource: a status pipe and a control pipe.

The status pipe carries events from the server to the AppHost:

The control pipe carries commands in the opposite direction:

This protocol is what enables selective hot reload. When you change a file in your API project, the server applies the delta only to that resource. Your frontend Blazor app, your background worker, and your database all remain untouched. If the change is a rude edit — something hot reload cannot handle — only the affected resource is rebuilt and restarted, not the entire application model.

What hot reload looks like now

With the integration enabled, the developer workflow changes substantially. Consider a typical Aspire solution with an AppHost, an API project, and a Blazor frontend:

AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.Api>("api");

builder.AddProject<Projects.Frontend>("frontend")
    .WithReference(api);

builder.Build().Run();

Previously, running dotnet watch on this AppHost meant every file change — in any project — triggered a full restart. Now, the flow is:

  1. You edit a controller in the API project and save.
  2. dotnet watch detects the change and determines it is hot-reload compatible.
  3. The server applies the delta to the running API process via the named pipe.
  4. The dashboard shows a hot_reload_applied status for the API resource.
  5. The frontend continues running without interruption.

For rude edits — adding a new class, changing method signatures, modifying startup code — the server rebuilds and restarts only the affected resource:

  1. You add a new middleware class to the API project.
  2. dotnet watch detects a rude edit.
  3. The server sends restarting on the status pipe, rebuilds the API project, and launches a new process.
  4. The frontend, still running, picks up the new API endpoint as soon as the process starts accepting connections.

The AppHost itself only restarts when its own files change — the orchestration code in Program.cs or changes to appsettings.json. And even then, DCP and the dashboard persist.

Crash recovery: deferred relaunch

The second improvement in Preview 3 addresses a gap in the existing dotnet watch behaviour. Previously, when a watched process crashed (exited with a non-zero exit code), dotnet watch would either stop entirely or enter a confused state. You had to manually restart it.

Now, dotnet watch records the crash and waits. When you next save a file change in the project or any of its dependencies, it triggers a rebuild and relaunch automatically.

This is a deliberate design choice — the relaunch is deferred to the next file change rather than being immediate. An immediate relaunch would risk infinite crash loops: if the code has a bug that crashes on startup, relaunching it immediately would just crash again. By waiting for a file change, the tool assumes you have made a fix attempt before it tries again.

The implementation handles multiple crashes independently. If both your API and worker services crash, each waits for its own relevant file change before relaunching. A change to a shared library project triggers relaunches for all crashed services that depend on it.

In practice, the workflow feels natural:

  1. Your API service throws an unhandled exception on startup and exits with code 1.
  2. dotnet watch logs the crash and enters a waiting state for that resource.
  3. You find the bug, fix it, and save the file.
  4. dotnet watch detects the change, rebuilds, and relaunches the API service.

No manual intervention required. No restarting dotnet watch itself. Combined with the Aspire integration, this means a crashed resource does not affect other services in the application model — they keep running while you fix the issue.

Ctrl+C on Windows desktop apps

The third improvement fixes a long-standing frustration for developers working with WinForms, WPF, or MAUI applications under dotnet watch.

The problem was twofold. First, Windows desktop frameworks disable console Ctrl+C handling during execution — it is part of how GUI applications work. Second, dotnet watch launches child processes in a process group, which disables Ctrl+C handlers by default. The result: pressing Ctrl+C in the terminal did nothing. You had to close the window manually or kill the process from Task Manager.

Preview 3 fixes this with two complementary changes.

For dotnet run with WinExe output types, the tooling now uses Process.CloseMainWindow() instead of sending console signals. This sends a WM_CLOSE message — the same message Windows sends when you click the X button — which GUI frameworks handle natively:

dotnet-watch/RunCommand.cs (simplified)
if (outputType == "WinExe")
{
    // Send WM_CLOSE instead of Ctrl+C for GUI apps
    process.CloseMainWindow();
}

For the process group issue, dotnet watch now calls SetConsoleCtrlHandler(null, false) at startup to re-enable default Ctrl+C handling within the group:

Example.cs
// Re-enable Ctrl+C in the process group (Windows only)
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCtrlHandler(Delegate? handler, bool add);

SetConsoleCtrlHandler(null, false);

This is a no-op on non-Windows platforms. The change means that pressing Ctrl+C in the terminal now gracefully shuts down WinForms and WPF applications instead of leaving orphaned processes.

Environment variables and configuration

The dotnet watch integration relies on several environment variables, some pre-existing and some new:

Variable Purpose
DOTNET_WATCH Set to 1 on all child processes launched by dotnet watch
DOTNET_WATCH_ITERATION Incremented on each restart or hot reload cycle
DOTNET_WATCH_RESTART_ON_RUDE_EDIT When true, auto-restarts on rude edits without prompting
DOTNET_HOTRELOAD_NAMEDPIPE_NAME The named pipe identifier for hot reload communication
DCP_KUBECONFIG_PATH Signals CLI-owned mode — Aspire detects this to skip launching its own DCP instance

The --non-interactive flag, available since .NET 7, remains the recommended way to run dotnet watch in CI or automated environments. It suppresses rude edit prompts and auto-restarts instead. In the Aspire context, this flag applies to the AppHost watch session.

// TIP

Use DOTNET_USE_POLLING_FILE_WATCHER=true when running inside Docker containers or on network shares. The default FileSystemWatcher does not detect changes reliably on mounted volumes.

Common pitfalls

Expecting hot reload to work with AppHost changes. Changes to the AppHost's own Program.cs still trigger a full AppHost restart. The selective hot reload only applies to resource projects. If you are frequently changing orchestration code, consider extracting resource configuration into extension methods so the core Program.cs stabilises early.

Mixing DateTime.Now with crash recovery. If your service uses DateTime.Now at startup to set a cache expiry or schedule, a crash and deferred relaunch will use a later timestamp. This is not a dotnet watch bug, but it can produce surprising behaviour in development. Use TimeProvider for testable, consistent time handling.

Forgetting that DCP persists across AppHost restarts. Because DCP and the dashboard survive AppHost restarts, resource registrations from previous runs can linger. If you rename a resource in the AppHost, the old name may appear briefly in the dashboard before the new registration replaces it. A full restart with Ctrl+C and re-run clears this state.

Assuming all file types trigger rebuilds. By default, dotnet watch monitors *.cs, *.csproj, *.resx, and wwwroot/** files. Changes to appsettings.json, *.json, or other configuration files do not trigger hot reload or restart unless you add them to the Watch item group in your project file:

Api/Api.csproj
<ItemGroup>
  <Watch Include="appsettings*.json" />
</ItemGroup>

Ignoring the --non-interactive flag in CI. Without this flag, dotnet watch will hang waiting for console input when it encounters a rude edit. Always use --non-interactive in automated build and test pipelines.

Summary