📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

πŸ“š Interview question

What is .NET Hot Reload, how does it work under the hood, and when does it fall back to a restart?

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.

Mid-Level Theory Advanced debugging dotnet-watch edit-and-continue hot-reload ide

🎯 Key points

  • Applies code edits to a running process without restarting, keeping app state
  • Introduced with .NET 6 and Visual Studio 2022; for .NET 10 use Visual Studio 2026, a current Rider, VS Code with C# Dev Kit, or dotnet watch
  • Roslyn compiles a delta (metadata, IL and PDB) and the runtime applies it through the metadata update API
  • Edit and Continue is the debugger-based form; Hot Reload also works without a debugger
  • Rude edits change type structure the runtime cannot patch safely, so they need a rebuild and restart
  • Code already running keeps the old version; changes take effect on the next call
  • MetadataUpdateHandler lets code clear caches after an update
  • Development only: Debug builds, never enabled in production images

⏱ 30-second answer

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.

πŸ—£ Full answer (senior)

How I would answer this in an interview

"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 MetadataUpdateHandler to clear it, and I remember that start-up code in Program.cs doesn'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."


What it is for

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.

Where it works

Hot Reload for managed code arrived with .NET 6 and Visual Studio 2022. For .NET 10 projects use tooling that supports .NET 10:

  • Visual Studio 2026: the Hot Reload button or apply on save, with or without the debugger attached.
  • Rider (a current release): applies changes while debugging.
  • VS Code with C# Dev Kit: while debugging.
  • dotnet watch from the .NET SDK: it watches files, applies deltas when it can, and offers to restart the app on a rude edit.

How it works

  1. You edit and save.
  2. Roslyn compares the new compilation with the running one and emits a delta: changed metadata, IL for changed methods, and PDB updates.
  3. The tool sends the delta to the process, where the runtime applies it with 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.
  4. The next call to an updated method runs the new IL. Frames already executing keep the old version (the debugger can sometimes remap the active statement).
  5. Registered MetadataUpdateHandler types are notified so frameworks and your code can clear caches or refresh UI.

Rude edits

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:

  • changing a class's base type or the interfaces it implements, because existing objects were laid out for the old hierarchy;
  • removing a method, field or property that running code or other assemblies may still reference;
  • some signature changes, where existing call sites were compiled against the old signature.

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.

Production example

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.

πŸ” What the interviewer is checking

The interviewer is checking understanding beyond "it reloads my code": the delta mechanism, its limits and practical workarounds.

Green flags

  • Explains Roslyn deltas and the runtime's metadata update API.
  • Knows rude edits and that running frames keep old code.
  • Knows caches and start-up code need special handling (MetadataUpdateHandler).

Red flags

  • Thinks it reloads the whole process.
  • Believes it can be used in production.
  • No idea why changes sometimes do not show.

πŸ“š Learn in depth

1. The end-to-end pipeline

  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.

2. The pieces and what each one does

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

3. Edit and Continue versus Hot Reload

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.

4. What is supported and what is rude

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.

5. Running code keeps the old version

 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.

6. Why caches go stale

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.

7. dotnet watch behaviour

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

8. Diagnosing "Hot Reload isn't working"

  1. Check the IDE's Hot Reload output pane, or the dotnet watch console, for "changes applied" or a rude edit message.
  2. Confirm it's a Debug build. Release builds are not a supported target.
  3. Put a breakpoint in the edited method. If it isn't hit, the code isn't being called: look for a cache or for start-up code.
  4. Check whether the code runs in a loop that never returns (section 5).
  5. Check the target framework and runtime are supported by your IDE version.
  6. Restart. If the change works after a restart, it was a cache, start-up code or a rude edit, not a bug in your change.

9. Performance and security

  • Applying a delta is usually quick, but a hot-reloaded process isn't representative of production: it's a Debug build, modifiable assemblies lose some optimisations, and each update adds metadata that stays in memory. Never profile or benchmark a session that has been hot reloaded; restart in Release first.
  • In a long session with many edits, memory use creeps up. Restart occasionally.
  • Never set 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.
  • Use dotnet watch on developer machines only, not on shared servers.

10. .NET 11 awareness

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.

11. Try it yourself

  1. Create dotnet new web -n HotDemo -f net10.0 and map /price to PricingRules.Calculate(100m).
  2. Run dotnet watch, change the rule, and refresh to see the new value without a restart.
  3. Cache the result in a static dictionary and see that the change no longer shows.
  4. Add the MetadataUpdateHandler from the code example and confirm it works again.
  5. Add a new method and call it: it applies in place.
  6. Change the class's base type and watch dotnet watch report a rude edit and offer to restart.
  7. Change a value read in Program.cs at start-up and see that nothing changes until you press Ctrl+R.

πŸ’» Code

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();

⚠ Common mistakes

  • 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.

πŸ› Senior insight: trade-offs

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.

πŸ”— Follow-ups the interviewer may ask

  1. When: The candidate mentions Edit and Continue while paused in the debugger. Why: Checks the debugger basics that Edit and Continue builds on.
    How to answer

    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.

  2. When: The candidate says Hot Reload only works on Debug builds. Why: Checks what actually differs between Debug and Release builds.
    How to answer

    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.

πŸ›  Real-world problems: how you handle them

Hot Reload "not working" because of a static rules cache
Situation
On a Blazor admin portal, developers used Hot Reload to fix a pricing calculation at the end of a six-step wizard. Edits to the calculation saved and applied without errors, but the page kept showing the old totals.
Signals
No rude edit message, the IDE reported the update as applied, breakpoints in the edited method were not hit on refresh, and a full restart showed the new values.
Investigation
Stepped through the request and found the totals came from a static `ConcurrentDictionary` filled on first use; the edited method only ran on a cache miss. Confirmed with a breakpoint on the cache lookup.
Resolution
Added a `MetadataUpdateHandler` that clears the rules cache after each update, and documented in the README which start-up code needs a restart.
Prevention
Added the handler pattern to the team's guidance for any static or memoised cache, and kept start-up time low so restarts are cheap when needed.

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].

More in IDEs and Editors

Last updated 1 Nov 2026.

🌐 Sangam's Webring

βͺ Previous 🏠 Main Next ⏩
πŸ’¬ Chat with me