Hot Reload in Visual Studio, Rider, VS Code and dotnet watch: Roslyn deltas applied through the runtime's metadata update API, Edit and Continue, rude edits, cache invalidation with MetadataUpdateHandler, and limits.
Hot Reload lets you change code while the app is running and see the effect without restarting, so you keep state such as a logged-in session. When you save, Roslyn compiles only the changed code into a delta of metadata, IL and debug information, and the runtime applies it through the metadata update API. It works in Visual Studio, Rider, VS Code and dotnet watch, with or without the debugger. Changes the runtime cannot apply, called rude edits, need a restart, and caches built from old code need clearing with a MetadataUpdateHandler. It is strictly a development tool.
"Hot Reload lets me change code while the app is running and see the effect without restarting, so I keep state such as a logged-in session or the last step of a wizard. It arrived with .NET 6, and for .NET 10 it works in Visual Studio 2026, a current Rider, VS Code with C# Dev Kit, and
dotnet watch.Under the hood, when I save, Roslyn compiles a delta of metadata, IL and PDB changes for only what changed, and the runtime applies it through the metadata update API,
MetadataUpdater.ApplyUpdate. It doesn't reload the process; it layers a new generation on top of the loaded assembly. Edit and Continue is the debugger-based form, used while I'm paused at a breakpoint, but Hot Reload also works without a debugger. Code that's already running keeps the old version, so the change shows on the next call.It falls back to a restart on rude edits, such as changing a base type or the interfaces a class implements, which the runtime can't patch safely into a live type system. The mistake I see is deciding Hot Reload is broken when a static cache is holding old results, so I add a
MetadataUpdateHandlerto clear it, and I remember that start-up code inProgram.csdoesn't re-run. It's development only: Debug builds, never enabled in production images, and never a session I'd measure performance in.Its value is proportional to how expensive it is to get back to the state I'm testing, so knowing when it won't help saves me from fighting it."
Hot Reload shortens the editβrun loop. Change a method body, a Razor component or an endpoint's logic, save, and the running app uses the new code. It matters most when getting back to the current state is slow: a multi-step UI flow, a debugger stopped deep in a request, or a service with slow start-up.
Hot Reload for managed code arrived with .NET 6 and Visual Studio 2022. For .NET 10 projects use tooling that supports .NET 10:
dotnet watch from the .NET SDK: it watches files, applies deltas when it can, and offers to restart the app on a rude edit.System.Reflection.Metadata.MetadataUpdater.ApplyUpdate. Without a debugger, the process must start with DOTNET_MODIFIABLE_ASSEMBLIES=debug, which dotnet watch and the IDEs set for you.MetadataUpdateHandler types are notified so frameworks and your code can clear caches or refresh UI.The runtime patches a live type system that already has objects, compiled code and reflection data built on the old shape. An edit is rude when applying it in place could leave that state inconsistent, for example:
Roslyn detects these before sending anything and the tool asks for a rebuild and restart. Each .NET release supports more edits (adding methods, fields and types is supported), so check the current documentation rather than memorising a list.
On a Blazor admin portal, reproducing a pricing bug needed six steps through a wizard. With Hot Reload we iterated on the calculation while staying on the last step. A teammate thought Hot Reload was broken because changes did not show; the rules were cached in a static dictionary at start-up. A MetadataUpdateHandler that cleared the cache fixed it.
The interviewer is checking understanding beyond "it reloads my code": the delta mechanism, its limits and practical workarounds.
Green flags
MetadataUpdateHandler).Red flags
edit + save
|
v
IDE or dotnet watch notices the change
|
v
Roslyn: compare the new compilation with the one the process is running
|
+-- rude edit? --yes--> report it --> rebuild + restart (state lost)
|
no
v
delta = metadata delta + IL delta + PDB delta
| sent to the process (debugger channel, or the dotnet watch agent)
v
runtime: MetadataUpdater.ApplyUpdate(assembly, metadataDelta, ilDelta, pdbDelta)
| throws if the assembly isn't modifiable or the delta doesn't match
v
[MetadataUpdateHandler] ClearCache(types) --> framework and your caches
[MetadataUpdateHandler] UpdateApplication(types) --> UI refresh (Blazor etc.)
|
v
next call to a changed method runs the new IL
Nothing is reloaded or recompiled from scratch. The runtime adds a new generation of metadata on top of the loaded assembly, and the JIT compiles the new IL the next time the method is called.
| Piece | Role |
|---|---|
| Roslyn (EnC engine) | Diffs the old and new syntax and semantic models, decides whether each edit is supported, and emits the delta |
| Runtime capabilities | The runtime reports which kinds of update it can apply. Roslyn checks every edit against this list before emitting anything |
| Delivery | With a debugger, the debugger applies the delta. Without one, dotnet watch injects a small agent (through DOTNET_STARTUP_HOOKS) that receives deltas and calls MetadataUpdater.ApplyUpdate |
| Modifiable assemblies | Updates only work if the process started with DOTNET_MODIFIABLE_ASSEMBLIES=debug. The IDEs and dotnet watch set it for you |
MetadataUpdateHandler |
An assembly attribute naming a type with static ClearCache(Type[]?) and/or UpdateApplication(Type[]?) methods, called after each update |
MetadataUpdater.IsSupported |
Lets code check at run time whether updates are possible in this process |
| Edit and Continue | Hot Reload | |
|---|---|---|
| Needs a debugger | Yes | No (also works with one) |
| When the edit applies | While paused, or on continue | On save or when you press the Hot Reload button |
| Active statement | The debugger can sometimes remap the paused frame to the new code | Frames already running keep the old code |
| Underlying mechanism | The same Roslyn deltas | The same Roslyn deltas, applied through MetadataUpdater |
In Visual Studio both are the same feature from the user's point of view. The distinction matters when you use dotnet watch or a process without a debugger attached.
The exact list depends on the runtime's capabilities, and it has grown with each release since .NET 6. As a guide for .NET 10 on CoreCLR:
| Usually applies in place | Usually a rude edit (restart needed) |
|---|---|
| Changing a method body, including async methods and iterators | Changing a type's base class or the interfaces it implements |
| Adding methods, properties, fields and nested types | Changing a type from class to struct, or similar shape changes |
| Adding new types | Some signature changes, where existing call sites were compiled against the old shape |
| Adding or changing lambdas and local functions | Changes the runtime reports it can't apply, for example on an older runtime |
| Changing string literals, conditions and loop logic | Edits to code that only runs at start-up (not rude, but has no visible effect) |
The last row is the most common source of confusion: an edit to Program.cs applies without error, but the code that uses it has already run.
Rude edits exist because the runtime is patching a live type system. Objects on the heap were laid out for the old type, JIT-compiled callers were built against the old signatures, and reflection data was cached. An edit that would make those inconsistent can't be applied safely, so Roslyn refuses it rather than corrupting the process.
Thread A is inside ProcessOrder() (old IL) when you save
|
+-- the current call finishes on the OLD code
|
v
next call to ProcessOrder() --> JIT compiles NEW IL --> new behaviour
A long-running loop such as a BackgroundService ExecuteAsync method may therefore never pick up the change, because it never returns. Move the loop body into a separate method that is called on each iteration, or restart.
Hot Reload replaces method bodies. It doesn't re-run constructors, static initialisers, Program.cs or the DI container build. Anything computed once from the old code keeps the old result:
| Stale after an update | Who clears it |
|---|---|
| Razor component render output | Blazor's own handler re-renders components |
| MVC and Razor Pages view and action caches | ASP.NET Core's handlers |
| Your static dictionaries, memoised results, compiled expression trees | You, with a MetadataUpdateHandler |
| DI registrations, middleware order, options bound at start-up | Nobody. Restart |
Frameworks register their own handlers. Your own caches need one, as in the code example.
| Situation | What dotnet watch does |
|---|---|
| Supported edit | Applies the delta and keeps the process running |
| Rude edit | Asks whether to restart. Set DOTNET_WATCH_RESTART_ON_RUDE_EDIT=1 to restart automatically |
| Project file or package change | Rebuilds and restarts |
| You want a clean start | Press Ctrl+R in the terminal to force a restart |
| You don't want Hot Reload | dotnet watch --no-hot-reload restarts on every change instead |
| Browser apps | Refreshes the browser through a local development connection |
dotnet watch console, for "changes applied" or a rude edit message.DOTNET_MODIFIABLE_ASSEMBLIES or a dotnet watch startup hook in production or container images. A process whose code can be patched at run time is a larger attack surface and no longer runs exactly the build you shipped and scanned.dotnet watch on developer machines only, not on shared servers.Hot Reload support keeps growing with each release, mostly through new runtime capabilities and better Razor and Blazor support. Check the .NET 11 release notes for newly supported edits rather than relying on an old list of rude edits.
dotnet new web -n HotDemo -f net10.0 and map /price to PricingRules.Calculate(100m).dotnet watch, change the rule, and refresh to see the new value without a restart.MetadataUpdateHandler from the code example and confirm it works again.dotnet watch report a rude edit and offer to restart.Program.cs at start-up and see that nothing changes until you press Ctrl+R.Run with Hot Reload from the CLI:
dotnet new web -n HotDemo -f net10.0
cd HotDemo
dotnet watch
PricingRules.cs, a cache that must be cleared when code changes:
using System.Collections.Concurrent;
using System.Reflection.Metadata;
[assembly: MetadataUpdateHandler(typeof(HotDemo.PricingRulesHotReload))]
namespace HotDemo;
public static class PricingRules
{
private static readonly ConcurrentDictionary<decimal, decimal> Cache = new();
public static decimal Calculate(decimal basePrice) =>
Cache.GetOrAdd(basePrice, price => Math.Round(price * 1.10m, 2)); // add 10% GST
internal static void Clear() => Cache.Clear();
}
internal static class PricingRulesHotReload
{
// Called by the runtime after a Hot Reload update is applied.
public static void ClearCache(Type[]? updatedTypes) => PricingRules.Clear();
}
Program.cs:
using HotDemo;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/price", (decimal? amount) => Results.Ok(new { total = PricingRules.Calculate(amount ?? 100m) }));
app.Run();
Mistake: expecting start-up code to re-run.
DI registrations, middleware order, options bound in Program.cs and static initialisers ran once with the old code. The edit applies, but nothing calls that code again.
Instead: restart after changes to start-up code (Ctrl+R in dotnet watch), and keep start-up fast so restarts are cheap.
Mistake: deciding Hot Reload is broken when a cache holds old results.
The edited method only runs on a cache miss, so the app keeps returning values computed by the old code.
Instead: add a MetadataUpdateHandler with a ClearCache method that clears your static or memoised caches. Use a breakpoint in the edited method to confirm whether it is being called.
Mistake: expecting a running method or loop to switch to the new code.
A frame already executing keeps the old IL. A BackgroundService loop that never returns never picks up the change.
Instead: move the loop body into a method called on each iteration, or restart. With the debugger paused, Edit and Continue can sometimes remap the current frame.
Mistake: fighting rude edits.
Changing a base type, interfaces or some signatures can't be patched into a live type system safely, and repeated attempts just waste time.
Instead: accept the restart. Set DOTNET_WATCH_RESTART_ON_RUDE_EDIT=1 so dotnet watch restarts automatically.
Mistake: relying on an old list of rude edits. Each .NET release adds supported edits, such as adding methods, fields, types and lambdas, so advice from the .NET 6 era is often out of date. Instead: try the edit and read the tool's message. Check the current documentation for your runtime.
Mistake: expecting Hot Reload on Release builds or in production.
It needs modifiable assemblies and Debug information, and it is a development tool.
Instead: use it in Debug on your own machine. Never set DOTNET_MODIFIABLE_ASSEMBLIES in production images.
Mistake: confusing a hot reload with a dotnet watch restart.
A restart loses in-memory state, sessions and the place you reached in a UI flow. A hot reload keeps them.
Instead: read the console: "changes applied" means hot reload; "restarting" means state is gone. Use --no-hot-reload only when you want a clean restart on every change.
Mistake: measuring performance in a hot-reloaded session. The process is a Debug build with extra metadata from every update and fewer optimisations, so numbers mean nothing. Instead: restart in Release, ideally with BenchmarkDotNet or a profiler, before you measure.
Mistake: thinking Hot Reload replaces the whole process or reloads the assembly. It only adds a new generation of metadata and IL on top of the loaded assembly; objects, statics and connections stay as they were. Instead: explain it as a delta applied by the runtime through the metadata update API, which is exactly why state survives and why caches go stale.
Hot Reload is a feedback-loop tool, and its value is proportional to how expensive it is to get back to the state you are testing. A senior engineer knows the mechanism well enough to predict when it will not help (start-up configuration, caches, rude edits) and does not waste time fighting it.
Performance implications. Applying a delta is usually quick, but the process is not representative of production while Hot Reload is active: it runs a Debug build, assemblies loaded as modifiable are compiled without some optimisations so their methods can be replaced, and every applied update adds metadata that stays in memory for the life of the process. Never take performance measurements or profile a session that has been hot reloaded; restart, ideally in Release, before you measure. In a long session with many edits, memory use creeps up and occasional restarts keep things predictable.
They also design code so it is friendly to fast feedback: fast start-up, small composable components, and tests that run in seconds, which help just as much as Hot Reload and also work in CI.
A breakpoint tells the debugger to pause the process when execution reaches a line, so I can look at variables and the call stack. I set one with F9 or a click in the gutter and start with F5. Then I step: Step Over (F10) runs the current line, Step Into (F11) goes into the method being called, and Step Out (Shift+F11) runs to the end of the current method and returns to the caller. Run to Cursor and Set Next Statement move faster or re-run a line. It works best on a Debug build with symbols, because optimised code can skip or reorder lines.
A configuration is a named set of build settings. Debug turns optimisation off and defines the DEBUG and TRACE symbols, so the debugger can show every variable and step line by line. Release turns optimisation on and defines RELEASE and TRACE instead of DEBUG. Both produce PDBs by default. Behaviour can differ for three reasons. Code under #if DEBUG, and calls such as Debug.Assert, are removed from Release. The optimising compiler and JIT inline and reorder code and end variable lifetimes earlier, which changes timing and when objects are garbage collected. And race conditions hidden by slow Debug code start to show. dotnet build defaults to Debug, while dotnet publish and dotnet pack default to Release, so you should test the build you ship.
How to tell it (STAR)
**Situation:** On [our admin portal], reproducing a pricing bug meant clicking through a six-step wizard, and Hot Reload seemed not to apply our fixes. **Task:** I wanted the fast feedback loop working so we could fix the bug quickly. **Action:** I traced the request and found the totals were cached in a static dictionary, so the edited method never re-ran. I added a `MetadataUpdateHandler` to clear the cache and wrote up which changes need a restart. **Result:** We fixed the bug in [an afternoon instead of a day], and the team stopped losing time to restarts [add your own measure].
Last updated 1 Nov 2026.