feat(HotReload): IHotReloadAware + MetadataUpdateHandler (aligns with dotnet/maui#35229)#254
feat(HotReload): IHotReloadAware + MetadataUpdateHandler (aligns with dotnet/maui#35229)#254StephaneDelcroix wants to merge 8 commits into
Conversation
Adds a new Microsoft.Maui.HotReload package providing a proper [assembly: MetadataUpdateHandler] implementation for .NET MAUI. ## What this adds ### IHotReloadable A lightweight interface with a single OnHotReload() method. Any class can implement it to receive a callback when its type is hot-reloaded. ### HotReloadRegistry A WeakReference-based instance registry. Instances that call HotReloadRegistry.Register(this) are tracked per-type. When a type is hot-reloaded, all live instances of that type (and subtypes) are notified. Ref-equality dedup prevents double-notification when both a base type and derived type appear in the updated types list. ### MauiMetadataUpdateHandler + AssemblyInfo The [assembly: MetadataUpdateHandler(typeof(MauiMetadataUpdateHandler))] attribute wires up the .NET hot reload host so it calls UpdateApplication and ClearCache automatically. On net10.0+, these delegate to MAUI's existing MauiHotReloadHelper for view-level reload. ### Microsoft.Maui.HotReload.SourceGen (Roslyn incremental generator) For partial IHotReloadable classes, auto-generates: private void HotReloadInitialize() => HotReloadRegistry.Register(this); Skips generic types and nested types (register manually). Emits MUH0001 Info diagnostic when no constructor calls HotReloadInitialize. Deduplicates multi-partial-declaration classes via Collect() + HashSet. ## Usage 1. Implement IHotReloadable on a partial class 2. Call HotReloadInitialize() in your constructor (source gen reminds you) 3. Override OnHotReload() to respond to hot reload Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new Microsoft.Maui.HotReload NuGet package plus a companion Roslyn incremental source generator (Microsoft.Maui.HotReload.SourceGen) to bridge .NET Hot Reload metadata update notifications into MAUI apps by routing updates to registered IHotReloadable instances (and forwarding to MAUI’s existing hot reload helper on net10.0).
Changes:
- Introduces
Microsoft.Maui.HotReload(netstandard2.0/net10.0) withIHotReloadable, a weak-reference instance registry, and aMetadataUpdateHandlerentrypoint. - Introduces
Microsoft.Maui.HotReload.SourceGento generate aHotReloadInitialize()registration helper and an informational diagnostic (MUH0001) when it isn’t called. - Adds both projects to
MauiLabs.slnx.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj | New shipping package project; multi-targeting and SourceGen analyzer reference. |
| src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs | Metadata update handler that notifies the registry and forwards to MauiHotReloadHelper on non-netstandard TFMs. |
| src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs | Public interface for user types to receive hot reload callbacks. |
| src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs | WeakReference-based registry for instances keyed by runtime type, with update notification dispatch. |
| src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs | Adds [assembly: MetadataUpdateHandler(...)] for non-netstandard builds. |
| src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj | New Roslyn component project with release-tracking files. |
| src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs | Incremental generator emitting HotReloadInitialize() and MUH0001 diagnostic. |
| src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Unshipped.md | Declares the new MUH0001 rule. |
| src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Shipped.md | Initializes shipped analyzer release tracking file. |
| MauiLabs.slnx | Adds the new HotReload projects to the solution. |
| string indent = info.Namespace is not null ? " " : string.Empty; | ||
| sb.AppendLine($"{indent}partial class {info.ClassName}"); | ||
| sb.AppendLine($"{indent}{{"); | ||
| sb.AppendLine($"{indent} /// <summary>Auto-generated. Call from your constructor to register this instance for hot reload notifications.</summary>"); | ||
| sb.AppendLine($"{indent} private void HotReloadInitialize()"); | ||
| sb.AppendLine($"{indent} => global::Microsoft.Maui.HotReload.HotReloadRegistry.Register(this);"); | ||
| sb.AppendLine($"{indent}}}"); |
| public static void UpdateApplication(Type[] types) | ||
| { | ||
| HotReloadRegistry.NotifyInstances(types); | ||
| #if !NETSTANDARD | ||
| MauiHotReloadHelper.UpdateApplication(types); | ||
| #endif |
| public static void ClearCache(Type[] types) | ||
| { | ||
| #if !NETSTANDARD |
| list.RemoveAt(i); | ||
| break; | ||
| } | ||
| } |
| @@ -0,0 +1,17 @@ | |||
| // <auto-generated/> | |||
Expert Code Review — PR #254Methodology: 3 independent reviewers with adversarial consensus Summary7 findings posted as inline comments: 5 moderate, 2 minor
Discarded findings (single reviewer, no consensus)
CI StatusCI is in progress — Test Coverage
Warning
|
There was a problem hiding this comment.
Expert Code Review: 7 findings posted inline (5 moderate, 2 minor). See the lean summary comment for full details.
Warning
⚠️ Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
learn.microsoft.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "learn.microsoft.com"See Network Configuration for more information.
Generated by Expert Code Review (auto) for issue #254 · ● 11.1M
| { | ||
| var seen = new System.Collections.Generic.HashSet<string>(StringComparer.Ordinal); | ||
| foreach (var info in allCandidates) | ||
| { | ||
| // Only emit once per unique class (multiple partial declarations yield the same HintName). | ||
| if (!seen.Add(info.HintName)) | ||
| continue; | ||
|
|
||
| EmitSource(ctx, info); |
There was a problem hiding this comment.
🟡 MODERATE · 3/3 consensus
MUH0001 false positive for multi-file partial classes. When a class is split across partial files (e.g., MyPage.xaml.cs with the constructor and MyPage.xaml.g.cs without), EmitAllSources picks whichever candidate appears first via seen.Add(). If the partial without a constructor is processed first, HasInitCall is false and MUH0001 fires — even though another partial file correctly calls HotReloadInitialize().
Scenario: Any XAML-backed page implementing IHotReloadable where the constructor lives in the code-behind will get a spurious diagnostic.
Fix: Merge HasInitCall across all candidates sharing the same HintName before emitting the diagnostic:
// In EmitAllSources, aggregate HasInitCall with OR across duplicates
var merged = new Dictionary<string, HotReloadableClassInfo>(StringComparer.Ordinal);
foreach (var info in allCandidates)
{
if (merged.TryGetValue(info.HintName, out var existing))
{
if (info.HasInitCall && !existing.HasInitCall)
merged[info.HintName] = new(..., hasInitCall: true);
}
else merged[info.HintName] = info;
}| <ProjectReference Include="..\Microsoft.Maui.HotReload.SourceGen\Microsoft.Maui.HotReload.SourceGen.csproj" | ||
| OutputItemType="Analyzer" | ||
| ReferenceOutputAssembly="false" /> |
There was a problem hiding this comment.
🟡 MODERATE · validated via follow-up (disputed: 2/3 agreed, 1 disagreed)
Source generator may not ship in NuGet package. One reviewer noted that OutputItemType="Analyzer" on a ProjectReference may not automatically pack the generator DLL into analyzers/dotnet/cs/ in the .nupkg — particularly with Arcade SDK pack targets. If so, consumers would get the runtime library but no source generator, making HotReloadInitialize() unavailable.
Note: This is disputed — the standard .NET SDK does flow OutputItemType="Analyzer" ProjectReferences into the package during pack. However, it's worth verifying with a dotnet pack and inspecting the output .nupkg to confirm the analyzer DLL is present. If not, add explicit pack items:
<None Include="$(OutputPath)\..\Microsoft.Maui.HotReload.SourceGen\**\*.dll"
Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />| /// Notifies all registered instances whose runtime type is, or derives from, any of the | ||
| /// <paramref name="updatedTypes"/>. Called by <see cref="MauiMetadataUpdateHandler"/>. | ||
| /// </summary> | ||
| internal static void NotifyInstances(Type[] updatedTypes) |
There was a problem hiding this comment.
🟢 MINOR · validated via follow-up (disputed: 2/3 agreed, 1 disagreed)
NotifyInstances lacks null guard. While the .NET runtime is unlikely to pass null to MetadataUpdateHandler methods in practice, adding a defensive check is low-cost and prevents an NRE from propagating to the hot reload host if invoked from a non-standard caller or test harness.
Fix:
internal static void NotifyInstances(Type[] updatedTypes)
{
if (updatedTypes is null || updatedTypes.Length == 0)
return;
// ...
}| foreach (var weakRef in kvp.Value) | ||
| { | ||
| if (!weakRef.TryGetTarget(out var instance)) | ||
| continue; |
There was a problem hiding this comment.
🟢 MINOR · 2/3 consensus
Dead WeakReference entries accumulate in NotifyInstances. When iterating kvp.Value, dead references are skipped (continue) but never removed. Cleanup only happens when a new instance of the same type calls Register(). For types created once at startup (singletons, app-level pages), dead refs from prior instances accumulate permanently.
Fix: Opportunistically compact after building toNotify, or collect dead indices during iteration and remove them under the same lock pass.
| sealed class HotReloadableClassInfo | ||
| { | ||
| public string? Namespace { get; } | ||
| public string ClassName { get; } | ||
| public string HintName { get; } | ||
| public Location Location { get; } | ||
| public bool HasInitCall { get; } | ||
|
|
||
| public HotReloadableClassInfo(string? ns, string className, string hintName, Location location, bool hasInitCall) | ||
| { | ||
| Namespace = ns; | ||
| ClassName = className; | ||
| HintName = hintName; | ||
| Location = location; | ||
| HasInitCall = hasInitCall; | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 MODERATE · validated via follow-up (2/2 agreed)
Incremental generator not truly incremental. HotReloadableClassInfo lacks Equals/GetHashCode overrides, so Roslyn's incremental pipeline uses reference equality — which always fails for newly created instances. Combined with storing Location (which changes across compilations even without source changes), the EmitAllSources callback re-fires unnecessarily on every compilation pass where any IHotReloadable file is touched.
Impact: In a MAUI project with many pages, this causes constant re-emission of all generated files during IDE editing, defeating the incremental generator's caching.
Fix: Make HotReloadableClassInfo a record (or implement IEquatable<T>) with value equality on Namespace, ClassName, HintName, and HasInitCall. Move Location out of the cached model into a separate pipeline for diagnostic reporting.
| foreach (var instance in toNotify) | ||
| { | ||
| try | ||
| { | ||
| instance.OnHotReload(); |
There was a problem hiding this comment.
🟡 MODERATE · 2/3 consensus
OnHotReload() invoked on background thread. The .NET hot reload host calls MetadataUpdateHandler methods on a non-UI thread. Realistic OnHotReload() implementations will update UI (rebind ViewModels, set properties on views), which will throw platform threading exceptions (UIKitThreadAccessException on iOS, CalledFromWrongThreadException on Android).
Note: MauiHotReloadHelper.UpdateApplication (called afterward) dispatches internally, but NotifyInstances does not.
Fix: Either dispatch to the UI thread before invoking callbacks:
Microsoft.Maui.ApplicationModel.MainThread.BeginInvokeOnMainThread(() =>
{
foreach (var instance in toNotify)
instance.OnHotReload();
});Or clearly document in IHotReloadable.OnHotReload() that implementations must self-dispatch to the main thread.
|
|
||
| // Compact dead references before adding to keep the list small. | ||
| list.RemoveAll(static w => !w.TryGetTarget(out _)); | ||
| list.Add(new WeakReference<IHotReloadable>(instance)); |
There was a problem hiding this comment.
🟡 MODERATE · 2/3 consensus
Duplicate registration not prevented. Register() compacts dead references but never checks for an existing live reference to the same instance. If HotReloadInitialize() is called more than once (e.g., from both a base-class and derived-class constructor, or on page re-use), the same instance is added multiple times. Each subsequent NotifyInstances call will invoke OnHotReload() N times on that instance.
Scenario: Base class constructor calls HotReloadInitialize(), derived class also calls it → double notification on every hot reload.
Fix: Before appending, scan for the instance:
list.RemoveAll(static w => !w.TryGetTarget(out _));
// Add dedup check:
foreach (var wr in list)
if (wr.TryGetTarget(out var existing) && ReferenceEquals(existing, instance))
return;
list.Add(new WeakReference<IHotReloadable>(instance));- Solution filter: src/HotReload/HotReload.slnf - CI workflow: .github/workflows/ci-hotreload.yml (uses _build.yml) - Product README: src/HotReload/README.md - AzDO pipeline: build job + publish stage in devflow-official.yml - Root README: HotReload product listing - AGENTS.md: HotReload product entry and project layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@StephaneDelcroix we had talked about maybe having some parameter on the method to help differentiate between "xaml hot reload took care of everything" reloads and others? |
Avoids future conflicts with a potential Microsoft.Maui.HotReload package shipping from the main dotnet/maui repository. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Without this, the SourceGen DLL was not embedded in the package and HotReloadInitialize() would not be generated for consumers. Uses MSBuild GetTargetPath to locate the DLL regardless of build artifacts layout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use versioned NuGet path (roslyn4.0/cs/) instead of generic analyzers/dotnet/cs/ because .NET 10+ SDK sets SupportsRoslynComponentVersioning=true, which causes it to ignore the generic path entirely. - Pin Roslyn reference to 4.4.0 (CLR 4.4.0.0), matching what Microsoft.Extensions.Options.SourceGeneration does. The Roslyn AnalyzerAssemblyLoader redirects these references to the host Roslyn version at runtime, so 4.4.0.0 works with Roslyn 5.x SDKs (SDK 10 and SDK 11). Using 5.x was wrong because the loader does not redirect across major versions in preview SDKs. - Remove debug canary (RegisterPostInitializationOutput emitting Canary.g.cs). Verified: generator produces HotReloadInitialize() for classes implementing IHotReloadable on both SDK 10 (net10.0) and SDK 11 (net11.0-android). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…alize call - Bump severity from Info to Warning so the diagnostic is visible in CLI builds (Info-level diagnostics are only surfaced by IDEs). - Replace the substring check on the constructor body text with a SyntaxWalk over IdentifierNameSyntax descendants. The previous check would treat a comment that merely mentioned 'HotReloadInitialize' as a real call, suppressing the warning when it should have fired. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… types Aligns the API surface with the design tracked in: dotnet/maui#35229 (Hot Reload Developer Experience epic) dotnet/maui#35227 (Design IHotReloadAware interface) dotnet/maui#35278 (Framework MetadataUpdateHandler) API changes: - Rename interface IHotReloadable -> IHotReloadAware - Change OnHotReload() -> OnHotReload(Type[]? updatedTypes), forwarding the same type info the runtime passes to MetadataUpdateHandler.UpdateApplication - Rename source file IHotReloadable.cs -> IHotReloadAware.cs - Rename generator file/class HotReloadableGenerator -> HotReloadAwareGenerator - Update MUH0001 diagnostic messages and generator type lookup accordingly Drive-by fix: - The net10.0 build of MauiMetadataUpdateHandler was silently broken because MauiHotReloadHelper lives in Microsoft.Maui.HotReload, which is a sibling (not ancestor) namespace of Microsoft.Maui.Labs.HotReload — C# parent-namespace lookup never resolved it. Add 'using Microsoft.Maui.HotReload;' inside the existing #if !NETSTANDARD block. The netstandard2.0 ref-assembly that ships was unaffected, so this was latent. Verified: gen-smoke project (SDK 10) emits HotReloadInitialize() for IHotReloadAware classes, MUH0001 fires when constructor doesn't call it, OnHotReload(Type[]?) signature compiles cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers
left a comment
There was a problem hiding this comment.
I got this to work with dotnet watch in .NET 11 Preview 4:
But I had to do this:
public partial class MainPage : ContentPage, IHotReloadAware
{
public MainPage()
{
InitializeComponent();
HotReloadInitialize(); // generated by MUH source generator
}
public void OnHotReload(Type[]? updatedTypes)
{
System.Diagnostics.Debug.WriteLine("MainPage was hot-reloaded!");
Device.BeginInvokeOnMainThread(() =>
{
// Re-initialize the page to pick up any changes to XAML or code.
InitializeComponent();
});
}I also had to remove the x:Name on the page, or I got an exception.
Am I using it right?
|
Running it on Avalonia.Controls.Maui and Peppers change above, it worked out of the box for me with our default TFM and Avalonia handlers,
At least with the basic smoke test template. I did get an exception running it on Catalyst both in this sandbox and in a standalone new MAUI template, at least on net11.0-maccatalyst Note that it is located in my local dotnet folder. The dll is there and it should have permissions to access it (It works fine with the default TFM) so I'm not sure why it's failing. But that's an SDK issue I think. |
|
@copilot resolve the merge conflicts in this pull request |
Co-authored-by: jfversluis <939291+jfversluis@users.noreply.github.com>
Resolved the merge conflicts by merging Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Adds IHotReloadAware integration on BaristaApp and ThemeService for use with .NET 11 Preview 4 'dotnet watch'. - Add Debug-only PackageReference to Microsoft.Maui.Labs.HotReload 0.1.0-dev (from local feed; built from dotnet/maui-labs#254). - ThemeService: partial class, HotReloadInitialize() called from ctor, OnHotReload logs updated types via ILogger. - BaristaApp: partial class implementing IHotReloadAware. MauiReactor's ComponentPartialClassSourceGenerator already emits the parameterless ctor, so HotReloadInitialize() is invoked from OnMounted instead. MUH0001 is suppressed at the class declaration because the labs source generator only walks constructors. - OnHotReload calls Invalidate() on the component to force a re-render. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Summary
Adds a new
Microsoft.Maui.Labs.HotReloadpackage to maui-labs that wires up[assembly: MetadataUpdateHandler]and exposes a developer-friendlyIHotReloadAwareinterface. Lets any instance — not just views — react when its type is updated by .NET Hot Reload.This is a maui-labs proving ground for the design tracked by:
IHotReloadAwareinterfaceMetadataUpdateHandlerfor single-page Hot Reload refreshPer #35278, the package owns the delivery mechanism (the
MetadataUpdateHandler); per #35227, it exposes a developer hook (IHotReloadAware) that any class can implement.Background
MauiHotReloadHelper.UpdateApplication/ClearCacheinMicrosoft.Maui.Corehave the right shape but the assembly-level[MetadataUpdateHandler]attribute was never added, so the .NET hot reload host never called them. This package fixes that and extends the pattern to any user type via a registry.What's included
Microsoft.Maui.Labs.HotReload(TFMs:netstandard2.0,net10.0)IHotReloadAwarevoid OnHotReload(Type[]? updatedTypes)HotReloadRegistryWeakReference-based per-type instance registryMauiMetadataUpdateHandlerMauiHotReloadHelperAssemblyInfo.cs[assembly: MetadataUpdateHandler(...)](gated to non-netstandard)The
Type[]? updatedTypesargument is forwarded straight from the runtime'sMetadataUpdateHandler.UpdateApplication(Type[]?)callback so users see exactly what changed.The registry uses ref-equality dedup so an instance is never notified twice when both its base type and its own type appear in the updated types list.
Microsoft.Maui.Labs.HotReload.SourceGen(Roslyn incremental generator,netstandard2.0)For any
partialclass implementingIHotReloadAware, auto-generates:MUH0001Warning when no constructor callsHotReloadInitialize()(uses a syntax walk, not a substring scan, so comments/docs mentioning the name don't suppress the warning)Usage
Notes
Microsoft.Maui.Labs.HotReload(notMicrosoft.Maui.HotReload) to avoid future conflicts when this graduates into the maindotnet/mauirepo per #35229.#if !NETSTANDARD-gated to avoid release-build impact in unsupported TFMs.