From e2d71987dc73a29b7951ea313849f0ee446fd7c0 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Thu, 7 May 2026 14:59:37 +0200 Subject: [PATCH 1/7] feat: add Microsoft.Maui.HotReload package with MUH and source generator 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> --- MauiLabs.slnx | 4 + .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 8 + .../HotReloadableGenerator.cs | 168 ++++++++++++++++++ .../Microsoft.Maui.HotReload.SourceGen.csproj | 24 +++ .../Microsoft.Maui.HotReload/AssemblyInfo.cs | 5 + .../HotReloadRegistry.cs | 129 ++++++++++++++ .../IHotReloadable.cs | 17 ++ .../MauiMetadataUpdateHandler.cs | 43 +++++ .../Microsoft.Maui.HotReload.csproj | 38 ++++ 10 files changed, 439 insertions(+) create mode 100644 src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Shipped.md create mode 100644 src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Unshipped.md create mode 100644 src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs create mode 100644 src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj create mode 100644 src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs create mode 100644 src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs create mode 100644 src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs create mode 100644 src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs create mode 100644 src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj diff --git a/MauiLabs.slnx b/MauiLabs.slnx index 576edb0c8..1773351ea 100644 --- a/MauiLabs.slnx +++ b/MauiLabs.slnx @@ -14,6 +14,10 @@ + + + + diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Shipped.md b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..7519a8a8d --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Unshipped.md b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..5e79268c7 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MUH0001 | HotReload | Info | HotReloadInitialize not called in constructor diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs new file mode 100644 index 000000000..a5afebbe1 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs @@ -0,0 +1,168 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.Maui.HotReload.SourceGen +{ + /// + /// Generates a HotReloadInitialize() method for every partial class that + /// implements Microsoft.Maui.HotReload.IHotReloadable. Also emits an informational + /// diagnostic if no constructor appears to call it. + /// + [Generator] + public sealed class HotReloadableGenerator : IIncrementalGenerator + { + const string IHotReloadableFullName = "Microsoft.Maui.HotReload.IHotReloadable"; + + static readonly DiagnosticDescriptor MissingInitCallDescriptor = new( + id: "MUH0001", + title: "HotReloadInitialize not called", + messageFormat: "'{0}' implements IHotReloadable but no constructor calls HotReloadInitialize(). Add a call to HotReloadInitialize() in your constructor to enable hot reload notifications.", + category: "HotReload", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Types implementing IHotReloadable must call the generated HotReloadInitialize() in their constructor for hot reload registration to take effect."); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var candidates = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: IsPartialClassDeclaration, + transform: GetHotReloadableClassInfo) + .Where(static info => info is not null) + .Select(static (info, _) => info!); + + // Deduplicate: a class split across multiple partial declarations will produce multiple + // candidates with the same HintName. Only the first one should emit source. + context.RegisterSourceOutput(candidates.Collect(), EmitAllSources); + } + + static bool IsPartialClassDeclaration(SyntaxNode node, System.Threading.CancellationToken _) => + node is ClassDeclarationSyntax c && c.Modifiers.Any(SyntaxKind.PartialKeyword); + + static HotReloadableClassInfo? GetHotReloadableClassInfo(GeneratorSyntaxContext ctx, System.Threading.CancellationToken ct) + { + var classDecl = (ClassDeclarationSyntax)ctx.Node; + if (ctx.SemanticModel.GetDeclaredSymbol(classDecl, ct) is not INamedTypeSymbol classSymbol) + return null; + + // Skip generic types and nested types: the simple partial class codegen doesn't + // handle type parameters or containing-type wrappers. Users of those shapes must + // call HotReloadRegistry.Register manually. + if (classSymbol.IsGenericType || classSymbol.ContainingType != null) + return null; + + var iface = ctx.SemanticModel.Compilation.GetTypeByMetadataName(IHotReloadableFullName); + if (iface is null) + return null; + + bool implementsIHotReloadable = false; + foreach (var i in classSymbol.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(i, iface)) + { + implementsIHotReloadable = true; + break; + } + } + + if (!implementsIHotReloadable) + return null; + + // Check whether any constructor in this partial declaration calls HotReloadInitialize. + bool hasInitCall = false; + foreach (var member in classDecl.Members) + { + if (member is not ConstructorDeclarationSyntax ctor) + continue; + + var bodyText = ctor.Body?.ToString() ?? ctor.ExpressionBody?.ToString() ?? string.Empty; + if (bodyText.IndexOf("HotReloadInitialize", StringComparison.Ordinal) >= 0) + { + hasInitCall = true; + break; + } + } + + return new HotReloadableClassInfo( + ns: classSymbol.ContainingNamespace.IsGlobalNamespace + ? null + : classSymbol.ContainingNamespace.ToDisplayString(), + className: classSymbol.Name, + hintName: classSymbol.ToDisplayString().Replace('.', '_').Replace('<', '_').Replace('>', '_'), + location: classDecl.GetLocation(), + hasInitCall: hasInitCall); + } + + static void EmitAllSources(SourceProductionContext ctx, System.Collections.Immutable.ImmutableArray allCandidates) + { + var seen = new System.Collections.Generic.HashSet(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); + } + } + + static void EmitSource(SourceProductionContext ctx, HotReloadableClassInfo info) + { + var sb = new StringBuilder(); + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + sb.AppendLine(); + + if (info.Namespace is not null) + { + sb.AppendLine($"namespace {info.Namespace}"); + sb.AppendLine("{"); + } + + string indent = info.Namespace is not null ? " " : string.Empty; + sb.AppendLine($"{indent}partial class {info.ClassName}"); + sb.AppendLine($"{indent}{{"); + sb.AppendLine($"{indent} /// Auto-generated. Call from your constructor to register this instance for hot reload notifications."); + sb.AppendLine($"{indent} private void HotReloadInitialize()"); + sb.AppendLine($"{indent} => global::Microsoft.Maui.HotReload.HotReloadRegistry.Register(this);"); + sb.AppendLine($"{indent}}}"); + + if (info.Namespace is not null) + sb.AppendLine("}"); + + ctx.AddSource($"{info.HintName}.HotReload.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); + + if (!info.HasInitCall) + { + ctx.ReportDiagnostic(Diagnostic.Create( + MissingInitCallDescriptor, + info.Location, + info.ClassName)); + } + } + + 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; + } + } + } +} diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj new file mode 100644 index 000000000..7304cfbfd --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + Microsoft.Maui.HotReload.SourceGen + Microsoft.Maui.HotReload.SourceGen + true + true + false + enable + latest + + + + + + + + + + + + + diff --git a/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs b/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs new file mode 100644 index 000000000..77bfad252 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs @@ -0,0 +1,5 @@ +#if !NETSTANDARD +using System.Reflection.Metadata; + +[assembly: MetadataUpdateHandler(typeof(Microsoft.Maui.HotReload.MauiMetadataUpdateHandler))] +#endif diff --git a/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs b/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs new file mode 100644 index 000000000..12d82160d --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs @@ -0,0 +1,129 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace Microsoft.Maui.HotReload +{ + /// + /// Registry that tracks live instances so they can be notified + /// when their type is updated via .NET Hot Reload. + /// + /// + /// Instances are held via so registration does not prevent garbage collection. + /// + public static class HotReloadRegistry + { + static readonly object _lock = new(); + static readonly Dictionary>> _registry = new(); + + /// + /// Registers so it receives + /// callbacks when its type (or any base type) is updated at runtime. + /// + public static void Register(IHotReloadable instance) + { + if (instance is null) + return; + + var type = instance.GetType(); + lock (_lock) + { + if (!_registry.TryGetValue(type, out var list)) + _registry[type] = list = new List>(); + + // Compact dead references before adding to keep the list small. + list.RemoveAll(static w => !w.TryGetTarget(out _)); + list.Add(new WeakReference(instance)); + } + } + + /// + /// Removes from the registry. + /// Safe to call even if the instance was never registered. + /// + public static void Unregister(IHotReloadable instance) + { + if (instance is null) + return; + + var type = instance.GetType(); + lock (_lock) + { + if (!_registry.TryGetValue(type, out var list)) + return; + + for (int i = list.Count - 1; i >= 0; i--) + { + if (!list[i].TryGetTarget(out var target)) + { + // Remove dead references opportunistically. + list.RemoveAt(i); + } + else if (ReferenceEquals(target, instance)) + { + list.RemoveAt(i); + break; + } + } + } + } + + /// + /// Notifies all registered instances whose runtime type is, or derives from, any of the + /// . Called by . + /// + internal static void NotifyInstances(Type[] updatedTypes) + { + List? toNotify = null; + + lock (_lock) + { + foreach (var updatedType in updatedTypes) + { + foreach (var kvp in _registry) + { + // Notify instances whose concrete type is updatedType or a subtype of it. + if (!updatedType.IsAssignableFrom(kvp.Key)) + continue; + + foreach (var weakRef in kvp.Value) + { + if (!weakRef.TryGetTarget(out var instance)) + continue; + + // Deduplicate by reference to avoid double-notifying when both a base + // type and derived type appear in updatedTypes simultaneously. + toNotify ??= new List(); + bool alreadyQueued = false; + for (int i = 0; i < toNotify.Count; i++) + { + if (ReferenceEquals(toNotify[i], instance)) + { + alreadyQueued = true; + break; + } + } + if (!alreadyQueued) + toNotify.Add(instance); + } + } + } + } + + if (toNotify is null) + return; + + foreach (var instance in toNotify) + { + try + { + instance.OnHotReload(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[MauiHotReload] Error in OnHotReload for {instance.GetType()}: {ex}"); + } + } + } + } +} diff --git a/src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs b/src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs new file mode 100644 index 000000000..918c813c5 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs @@ -0,0 +1,17 @@ +// +#nullable enable +namespace Microsoft.Maui.HotReload +{ + /// + /// Implemented by types that want to be notified when their code is updated via .NET Hot Reload. + /// + /// + /// To register for notifications, call HotReloadInitialize() in your constructor + /// (generated automatically by Microsoft.Maui.HotReload.SourceGen for partial classes). + /// + public interface IHotReloadable + { + /// Called when this instance's type (or a base type) has been updated via hot reload. + void OnHotReload(); + } +} diff --git a/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs b/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs new file mode 100644 index 000000000..fd8188847 --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs @@ -0,0 +1,43 @@ +#nullable enable +using System; + +namespace Microsoft.Maui.HotReload +{ + /// + /// Handles .NET Hot Reload metadata update notifications, forwarding them to registered + /// instances and the MAUI view-level hot reload infrastructure. + /// + public static class MauiMetadataUpdateHandler + { + /// + /// Called by the .NET Hot Reload host after metadata has been applied. + /// Notifies all registered instances whose types were updated, + /// then forwards to MAUI's MauiHotReloadHelper for view-level reload. + /// + /// + /// This method is invoked only during hot reload sessions (debug/development builds). + /// Trimming and hot reload are mutually exclusive, so trim-compatibility warnings are suppressed. + /// +#if !NETSTANDARD + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hot Reload is not compatible with trimming.")] + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Hot Reload is not compatible with AOT.")] +#endif + public static void UpdateApplication(Type[] types) + { + HotReloadRegistry.NotifyInstances(types); +#if !NETSTANDARD + MauiHotReloadHelper.UpdateApplication(types); +#endif + } + + /// + /// Called by the .NET Hot Reload host before metadata is applied, to clear any caches. + /// + public static void ClearCache(Type[] types) + { +#if !NETSTANDARD + MauiHotReloadHelper.ClearCache(types); +#endif + } + } +} diff --git a/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj new file mode 100644 index 000000000..69b55479c --- /dev/null +++ b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj @@ -0,0 +1,38 @@ + + + + netstandard2.0;net10.0 + Microsoft.Maui.HotReload + Microsoft.Maui.HotReload + true + enable + latest + + + + + + + + + true + true + Microsoft.Maui.HotReload + Provides a MetadataUpdateHandler for .NET MAUI hot reload. Register any IHotReloadable instance to receive OnHotReload() callbacks when their type is hot-reloaded. + $(DefaultPackageTags);hotreload;developer-tools + true + + + + + + + + + + + + + From bdfcaf49a7b04dbf8e2910e8129f8a481edec11a Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Fri, 8 May 2026 14:00:57 +0200 Subject: [PATCH 2/7] Add release infrastructure for Microsoft.Maui.HotReload - 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> --- .github/workflows/ci-hotreload.yml | 34 +++++++++ AGENTS.md | 6 ++ README.md | 8 +++ eng/pipelines/devflow-official.yml | 87 ++++++++++++++++++++++- src/HotReload/HotReload.slnf | 9 +++ src/HotReload/README.md | 107 +++++++++++++++++++++++++++++ 6 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci-hotreload.yml create mode 100644 src/HotReload/HotReload.slnf create mode 100644 src/HotReload/README.md diff --git a/.github/workflows/ci-hotreload.yml b/.github/workflows/ci-hotreload.yml new file mode 100644 index 000000000..f2cfa452d --- /dev/null +++ b/.github/workflows/ci-hotreload.yml @@ -0,0 +1,34 @@ +name: CI - HotReload + +on: + push: + branches: [main] + paths: + - 'src/HotReload/**' + - 'eng/**' + - 'Directory.Build.props' + - 'Directory.Build.targets' + - 'Directory.Packages.props' + - 'global.json' + - 'NuGet.config' + pull_request: + types: [opened, synchronize, reopened, edited] + branches: [main] + paths: + - 'src/HotReload/**' + - 'eng/**' + - 'Directory.Build.props' + - 'Directory.Build.targets' + - 'Directory.Packages.props' + - 'global.json' + - 'NuGet.config' + +jobs: + build: + uses: ./.github/workflows/_build.yml + with: + project-path: src/HotReload/HotReload.slnf + project-name: hotreload + run-tests: false + pack: true + install-workloads: false diff --git a/AGENTS.md b/AGENTS.md index c251ffbe8..04ebf07c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ This repository hosts experimental .NET MAUI packages. It is a **multi-product m | **Cli** | `Microsoft.Maui.Cli` (global tool: `maui`) | Unified MAUI command-line tool: environment diagnostics (`maui doctor`), Android SDK/JDK/emulator management, Apple platform management, device listing, `maui go` for rapid prototyping, `maui profile startup` for performance tracing, and the `maui devflow` automation surface. | | **DevFlow** | `Microsoft.Maui.DevFlow.*` packages plus the unified `maui devflow` CLI surface | Runtime MAUI automation toolkit. In-app agent with HTTP API, visual tree inspection, CDP bridge for Blazor WebViews, MCP server for AI agents, cross-platform driver library. | | **Comet** | `Comet`, `Comet.SourceGenerator`, `Comet.Layout.Yoga` | Experimental MVU UI framework for .NET MAUI — C# fluent UI, signals/reactive state, Yoga layout. | +| **HotReload** | `Microsoft.Maui.HotReload` | MetadataUpdateHandler for .NET MAUI hot reload — `IHotReloadable` interface with per-instance `OnHotReload()` callbacks, backed by a Roslyn source generator. | | **Go** | `Microsoft.Maui.Go.Server` + Comet Go companion app | Single-file Comet apps server and companion app for rapid prototyping (alpha; sister to Comet). | ### Technology Stack @@ -109,6 +110,11 @@ maui-labs/ │ ├── Server/Microsoft.Maui.Go.Server/ # Comet Go server │ ├── CompanionApp/ # Comet Go companion MAUI app │ └── Shared/ # Shared Comet Go code +├── src/ +│ └── HotReload/ # HotReload product +│ ├── Microsoft.Maui.HotReload/ # Shipping package (IHotReloadable, registry, handler) +│ ├── Microsoft.Maui.HotReload.SourceGen/ # Roslyn source generator (bundled as analyzer) +│ └── HotReload.slnf # Solution filter ├── samples/ # Sample MAUI apps (not shipped) ├── playground/ # Manual test/scratch apps ├── eng/ # Shared build infrastructure diff --git a/README.md b/README.md index c9abc1d49..0ffbe2284 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,14 @@ Built artifacts are exposed as `@(MauiAppArtifact)` items with `ArtifactType`, ` |---------|-------------| | `Microsoft.Maui.Build.AppProjectReference` | Build-time app project reference with artifact discovery | +### HotReload + +A `MetadataUpdateHandler` for .NET MAUI that delivers per-instance hot reload callbacks via the `IHotReloadable` interface, backed by a Roslyn source generator. + +| Package | Description | +|---------|-------------| +| `Microsoft.Maui.HotReload` | MetadataUpdateHandler for .NET MAUI hot reload with `IHotReloadable` callbacks | + ## Agent Skills This repository is also a marketplace for distributable agent skills for .NET MAUI development. Skills are organized as plugins compatible with Copilot CLI, Claude Code, and VS Code. diff --git a/eng/pipelines/devflow-official.yml b/eng/pipelines/devflow-official.yml index 0ed97dd98..13250e8e6 100644 --- a/eng/pipelines/devflow-official.yml +++ b/eng/pipelines/devflow-official.yml @@ -40,6 +40,10 @@ parameters: displayName: 'Publish EssentialsAI packages to NuGet.org' type: boolean default: false +- name: publishHotReloadNuget + displayName: 'Publish HotReload packages to NuGet.org' + type: boolean + default: false variables: - template: /eng/pipelines/common-variables.yml@self @@ -359,7 +363,33 @@ extends: -projects $(Build.SourcesDirectory)\platforms\MacOS\MacOS-libs.slnf $(_OfficialBuildArgs) displayName: Build, Test and Pack macOS AppKit - + + # HotReload targets netstandard2.0 + net10.0 with no MAUI workloads. + # Builds and packs on Windows for MicroBuild/ESRP signing. + - job: HotReload + displayName: HotReload - Windows + pool: + name: NetCore1ESPool-Internal + demands: ImageOverride -equals windows.vs2026preview.scout.amd64 + strategy: + matrix: + Release: + _BuildConfig: Release + _OfficialBuildArgs: /p:DotNetSignType=$(_SignType) + /p:TeamName=$(_TeamName) + /p:OfficialBuildId=$(BUILD.BUILDNUMBER) + steps: + - task: UseDotNet@2 + displayName: Install .NET SDK + inputs: + version: 10.0.105 + - script: eng\common\cibuild.cmd + -configuration $(_BuildConfig) + -prepareMachine + -projects $(Build.SourcesDirectory)\src\HotReload\HotReload.slnf + $(_OfficialBuildArgs) + displayName: Build and Pack HotReload + - template: /eng/common/templates-official/post-build/post-build.yml@self parameters: enableSourceLinkValidation: false @@ -751,3 +781,58 @@ extends: packageParentPath: '$(Pipeline.Workspace)/MacOSPackages' nuGetFeedType: external publishFeedCredentials: 'nuget.org (dotnetframework)' + + # Publish HotReload packages to NuGet.org + - ${{ if eq(parameters.publishHotReloadNuget, true) }}: + - stage: publish_hotreload_nuget + displayName: 'Publish HotReload to NuGet.org' + dependsOn: + - Validate + - publish_using_darc + jobs: + - job: PrepareArtifacts + displayName: 'Prepare HotReload Artifacts' + timeoutInMinutes: 15 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + templateContext: + outputs: + - output: pipelineArtifact + displayName: Publish HotReload Packages + targetPath: '$(Pipeline.Workspace)/HotReloadPackages' + artifactName: HotReloadPackagesForNuGet + steps: + - download: current + artifact: PackageArtifacts + displayName: Download PackageArtifacts + - powershell: | + New-Item -ItemType Directory -Force -Path '$(Pipeline.Workspace)/HotReloadPackages' + Copy-Item '$(Pipeline.Workspace)/PackageArtifacts/Microsoft.Maui.HotReload.*.nupkg' '$(Pipeline.Workspace)/HotReloadPackages/' -Verbose + displayName: Filter HotReload packages + + - job: PublishNuGet + displayName: 'Push HotReload to NuGet.org' + dependsOn: PrepareArtifacts + timeoutInMinutes: 30 + pool: + name: NetCore1ESPool-Internal + image: windows.vs2026preview.scout.amd64 + os: windows + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: HotReloadPackagesForNuGet + targetPath: '$(Pipeline.Workspace)/HotReloadPackages' + steps: + - task: 1ES.PublishNuget@1 + displayName: 'Push HotReload to NuGet.org' + inputs: + useDotNetTask: false + packagesToPush: '$(Pipeline.Workspace)/HotReloadPackages/*.nupkg' + packageParentPath: '$(Pipeline.Workspace)/HotReloadPackages' + nuGetFeedType: external + publishFeedCredentials: 'nuget.org (dotnetframework)' diff --git a/src/HotReload/HotReload.slnf b/src/HotReload/HotReload.slnf new file mode 100644 index 000000000..121a4ecc7 --- /dev/null +++ b/src/HotReload/HotReload.slnf @@ -0,0 +1,9 @@ +{ + "solution": { + "path": "../../MauiLabs.slnx", + "projects": [ + "src\\HotReload\\Microsoft.Maui.HotReload\\Microsoft.Maui.HotReload.csproj", + "src\\HotReload\\Microsoft.Maui.HotReload.SourceGen\\Microsoft.Maui.HotReload.SourceGen.csproj" + ] + } +} diff --git a/src/HotReload/README.md b/src/HotReload/README.md new file mode 100644 index 000000000..7f6199857 --- /dev/null +++ b/src/HotReload/README.md @@ -0,0 +1,107 @@ +# Microsoft.Maui.HotReload + +A `MetadataUpdateHandler` for .NET MAUI that delivers per-instance hot reload callbacks. Implement `IHotReloadable` on any class, call the generated `HotReloadInitialize()` in its constructor, and receive `OnHotReload()` notifications whenever your type is modified during a hot reload session. + +> ⚠️ **Experimental** — APIs may change between releases. This package is not covered by the [.NET MAUI Support Policy](https://dotnet.microsoft.com/platform/support/policy/maui) and is provided as-is. + +## How It Works + +The package has two components: + +| Assembly | Role | +|----------|------| +| `Microsoft.Maui.HotReload` | Ships the `IHotReloadable` interface, `HotReloadRegistry`, and `MauiMetadataUpdateHandler` | +| `Microsoft.Maui.HotReload.SourceGen` | Roslyn incremental source generator bundled as an analyzer inside the NuGet package | + +When the .NET runtime applies a metadata update (hot reload), the `[assembly: MetadataUpdateHandler]` attribute routes the notification to `MauiMetadataUpdateHandler.UpdateApplication(Type[])`. This method: + +1. Walks the `HotReloadRegistry` to find all live `IHotReloadable` instances whose type (or a base type) was updated. +2. Calls `OnHotReload()` on each matched instance. +3. Forwards to MAUI's built-in `MauiHotReloadHelper` for view-level reload (on `net10.0`+). + +Instances are held via `WeakReference` so registration does not prevent garbage collection. + +## Quick Start + +Install the NuGet package: + +```bash +dotnet add package Microsoft.Maui.HotReload --prerelease +``` + +Implement `IHotReloadable` on a `partial` class and call `HotReloadInitialize()` in the constructor: + +```csharp +using Microsoft.Maui.HotReload; + +public partial class MainPage : ContentPage, IHotReloadable +{ + public MainPage() + { + HotReloadInitialize(); // Generated by the source generator + InitializeComponent(); + } + + public void OnHotReload() + { + // Re-apply dynamic state, refresh bindings, etc. + System.Diagnostics.Debug.WriteLine("MainPage was hot-reloaded!"); + } +} +``` + +That's it — during a hot reload session, `OnHotReload()` fires whenever `MainPage` (or any of its base types) is updated. + +## Source Generator + +The `Microsoft.Maui.HotReload.SourceGen` generator runs at compile time and, for every `partial` class implementing `IHotReloadable`: + +1. **Emits a `HotReloadInitialize()` method** that calls `HotReloadRegistry.Register(this)`. +2. **Reports diagnostic `MUH0001`** (Info severity) if no constructor in the class appears to call `HotReloadInitialize()`. + +### Limitations + +- **Generic types and nested types** are not supported by the generator. For those, call `HotReloadRegistry.Register(this)` manually in the constructor. +- The class **must be `partial`** for the generator to emit code into it. + +## Manual Registration + +For classes that cannot be `partial` or are generic/nested, register directly: + +```csharp +public class MyService : IHotReloadable +{ + public MyService() + { + HotReloadRegistry.Register(this); + } + + public void OnHotReload() + { + // Handle hot reload + } +} +``` + +Call `HotReloadRegistry.Unregister(this)` if you need to stop receiving notifications before the instance is garbage-collected. + +## Target Frameworks + +| TFM | Behavior | +|-----|----------| +| `net10.0` | Full integration — `MetadataUpdateHandler` registered, forwards to MAUI's `MauiHotReloadHelper` | +| `netstandard2.0` | `IHotReloadable` + `HotReloadRegistry` available for library authors; `MetadataUpdateHandler` not active | + +## Package + +| Package | Description | +|---------|-------------| +| `Microsoft.Maui.HotReload` | MetadataUpdateHandler for .NET MAUI hot reload with `IHotReloadable` callbacks | + +## Building + +```bash +dotnet build src/HotReload/HotReload.slnf +``` + +No MAUI workload install is required — the project targets `netstandard2.0` and `net10.0` with a conditional `Microsoft.Maui.Core` reference. From 9534ef59b2290b7218c100e47044f087dba2bb5d Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Tue, 12 May 2026 08:36:56 +0200 Subject: [PATCH 3/7] refactor: rename namespace to Microsoft.Maui.Labs.HotReload 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> --- .../HotReloadableGenerator.cs | 8 ++++---- .../Microsoft.Maui.HotReload.SourceGen.csproj | 4 ++-- src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs | 2 +- .../Microsoft.Maui.HotReload/HotReloadRegistry.cs | 2 +- src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs | 4 ++-- .../Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs | 2 +- .../Microsoft.Maui.HotReload.csproj | 6 +++--- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs index a5afebbe1..1b72fcdf5 100644 --- a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs @@ -7,17 +7,17 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.Maui.HotReload.SourceGen +namespace Microsoft.Maui.Labs.HotReload.SourceGen { /// /// Generates a HotReloadInitialize() method for every partial class that - /// implements Microsoft.Maui.HotReload.IHotReloadable. Also emits an informational + /// implements Microsoft.Maui.Labs.HotReload.IHotReloadable. Also emits an informational /// diagnostic if no constructor appears to call it. /// [Generator] public sealed class HotReloadableGenerator : IIncrementalGenerator { - const string IHotReloadableFullName = "Microsoft.Maui.HotReload.IHotReloadable"; + const string IHotReloadableFullName = "Microsoft.Maui.Labs.HotReload.IHotReloadable"; static readonly DiagnosticDescriptor MissingInitCallDescriptor = new( id: "MUH0001", @@ -130,7 +130,7 @@ static void EmitSource(SourceProductionContext ctx, HotReloadableClassInfo info) sb.AppendLine($"{indent}{{"); sb.AppendLine($"{indent} /// Auto-generated. Call from your constructor to register this instance for hot reload notifications."); sb.AppendLine($"{indent} private void HotReloadInitialize()"); - sb.AppendLine($"{indent} => global::Microsoft.Maui.HotReload.HotReloadRegistry.Register(this);"); + sb.AppendLine($"{indent} => global::Microsoft.Maui.Labs.HotReload.HotReloadRegistry.Register(this);"); sb.AppendLine($"{indent}}}"); if (info.Namespace is not null) diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj index 7304cfbfd..c97edafb9 100644 --- a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj @@ -2,8 +2,8 @@ netstandard2.0 - Microsoft.Maui.HotReload.SourceGen - Microsoft.Maui.HotReload.SourceGen + Microsoft.Maui.Labs.HotReload.SourceGen + Microsoft.Maui.Labs.HotReload.SourceGen true true false diff --git a/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs b/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs index 77bfad252..2d59ff52f 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs +++ b/src/HotReload/Microsoft.Maui.HotReload/AssemblyInfo.cs @@ -1,5 +1,5 @@ #if !NETSTANDARD using System.Reflection.Metadata; -[assembly: MetadataUpdateHandler(typeof(Microsoft.Maui.HotReload.MauiMetadataUpdateHandler))] +[assembly: MetadataUpdateHandler(typeof(Microsoft.Maui.Labs.HotReload.MauiMetadataUpdateHandler))] #endif diff --git a/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs b/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs index 12d82160d..f29353969 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs +++ b/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -namespace Microsoft.Maui.HotReload +namespace Microsoft.Maui.Labs.HotReload { /// /// Registry that tracks live instances so they can be notified diff --git a/src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs b/src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs index 918c813c5..4f7fbeeea 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs +++ b/src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs @@ -1,13 +1,13 @@ // #nullable enable -namespace Microsoft.Maui.HotReload +namespace Microsoft.Maui.Labs.HotReload { /// /// Implemented by types that want to be notified when their code is updated via .NET Hot Reload. /// /// /// To register for notifications, call HotReloadInitialize() in your constructor - /// (generated automatically by Microsoft.Maui.HotReload.SourceGen for partial classes). + /// (generated automatically by Microsoft.Maui.Labs.HotReload.SourceGen for partial classes). /// public interface IHotReloadable { diff --git a/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs b/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs index fd8188847..c548be23a 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs +++ b/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs @@ -1,7 +1,7 @@ #nullable enable using System; -namespace Microsoft.Maui.HotReload +namespace Microsoft.Maui.Labs.HotReload { /// /// Handles .NET Hot Reload metadata update notifications, forwarding them to registered diff --git a/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj index 69b55479c..a0ce58988 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj +++ b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj @@ -2,8 +2,8 @@ netstandard2.0;net10.0 - Microsoft.Maui.HotReload - Microsoft.Maui.HotReload + Microsoft.Maui.Labs.HotReload + Microsoft.Maui.Labs.HotReload true enable latest @@ -17,7 +17,7 @@ true true - Microsoft.Maui.HotReload + Microsoft.Maui.Labs.HotReload Provides a MetadataUpdateHandler for .NET MAUI hot reload. Register any IHotReloadable instance to receive OnHotReload() callbacks when their type is hot-reloaded. $(DefaultPackageTags);hotreload;developer-tools true From 9b92db16311315a847472d378b61c324d80cd57a Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Tue, 12 May 2026 08:49:04 +0200 Subject: [PATCH 4/7] fix: include source generator DLL in NuGet analyzers/dotnet/cs/ path 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> --- .../Microsoft.Maui.HotReload.csproj | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj index a0ce58988..c5d238840 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj +++ b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj @@ -35,4 +35,17 @@ ReferenceOutputAssembly="false" /> + + + + + + + + + + From 97e5054417780a85c06c117e20b52fbe580b6b06 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Tue, 12 May 2026 09:48:39 +0200 Subject: [PATCH 5/7] fix: source generator works via NuGet on .NET 10+ SDK - 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> --- .../HotReloadableGenerator.cs | 43 +++++++++++++++---- .../Microsoft.Maui.HotReload.SourceGen.csproj | 8 +++- .../Microsoft.Maui.HotReload.csproj | 8 ++-- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs index 1b72fcdf5..b189c564d 100644 --- a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs @@ -43,7 +43,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } static bool IsPartialClassDeclaration(SyntaxNode node, System.Threading.CancellationToken _) => - node is ClassDeclarationSyntax c && c.Modifiers.Any(SyntaxKind.PartialKeyword); + node is ClassDeclarationSyntax c && + c.Modifiers.Any(SyntaxKind.PartialKeyword) && + c.BaseList is not null; static HotReloadableClassInfo? GetHotReloadableClassInfo(GeneratorSyntaxContext ctx, System.Threading.CancellationToken ct) { @@ -57,17 +59,40 @@ static bool IsPartialClassDeclaration(SyntaxNode node, System.Threading.Cancella if (classSymbol.IsGenericType || classSymbol.ContainingType != null) return null; - var iface = ctx.SemanticModel.Compilation.GetTypeByMetadataName(IHotReloadableFullName); - if (iface is null) - return null; - + // Check interfaces: prefer semantic model, but fall back to syntax name matching + // so the generator works even when the referenced assembly isn't fully loaded + // in the compilation (e.g. net10.0 lib consumed by a net11.0-* project). bool implementsIHotReloadable = false; - foreach (var i in classSymbol.AllInterfaces) + + var iface = ctx.SemanticModel.Compilation.GetTypeByMetadataName(IHotReloadableFullName); + if (iface is not null) { - if (SymbolEqualityComparer.Default.Equals(i, iface)) + // Semantic path: exact type identity check + foreach (var i in classSymbol.AllInterfaces) { - implementsIHotReloadable = true; - break; + if (SymbolEqualityComparer.Default.Equals(i, iface)) + { + implementsIHotReloadable = true; + break; + } + } + } + else + { + // Syntax fallback: match by short or fully-qualified name in the base list + foreach (var baseType in classDecl.BaseList?.Types ?? default) + { + var typeName = baseType.Type switch + { + IdentifierNameSyntax id => id.Identifier.Text, + QualifiedNameSyntax q => q.Right.Identifier.Text, + _ => null + }; + if (typeName == "IHotReloadable") + { + implementsIHotReloadable = true; + break; + } } } diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj index c97edafb9..2da57d100 100644 --- a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/Microsoft.Maui.HotReload.SourceGen.csproj @@ -12,8 +12,12 @@ - - + + diff --git a/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj index c5d238840..2eacc81a4 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj +++ b/src/HotReload/Microsoft.Maui.HotReload/Microsoft.Maui.HotReload.csproj @@ -36,15 +36,17 @@ + Uses Build target (not GetTargetPath) to ensure the generator is up-to-date before packing. + Path must be roslyn4.0/cs/ (not the generic dotnet/cs/) because SDKs with + SupportsRoslynComponentVersioning=true only load from versioned paths. --> - + From 49ba92a76aac89791dce310d736b7646e2d85425 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Tue, 12 May 2026 09:56:47 +0200 Subject: [PATCH 6/7] MUH0001: warn (not info) and use syntax walk to detect HotReloadInitialize 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> --- .../HotReloadableGenerator.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs index b189c564d..98e54e2a5 100644 --- a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -24,7 +25,7 @@ public sealed class HotReloadableGenerator : IIncrementalGenerator title: "HotReloadInitialize not called", messageFormat: "'{0}' implements IHotReloadable but no constructor calls HotReloadInitialize(). Add a call to HotReloadInitialize() in your constructor to enable hot reload notifications.", category: "HotReload", - defaultSeverity: DiagnosticSeverity.Info, + defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Types implementing IHotReloadable must call the generated HotReloadInitialize() in their constructor for hot reload registration to take effect."); @@ -100,18 +101,29 @@ node is ClassDeclarationSyntax c && return null; // Check whether any constructor in this partial declaration calls HotReloadInitialize. + // Walk identifier tokens (skipping trivia/comments) so we don't get false positives + // from documentation or comments mentioning "HotReloadInitialize". bool hasInitCall = false; foreach (var member in classDecl.Members) { if (member is not ConstructorDeclarationSyntax ctor) continue; - var bodyText = ctor.Body?.ToString() ?? ctor.ExpressionBody?.ToString() ?? string.Empty; - if (bodyText.IndexOf("HotReloadInitialize", StringComparison.Ordinal) >= 0) + SyntaxNode? body = (SyntaxNode?)ctor.Body ?? ctor.ExpressionBody; + if (body is null) + continue; + + foreach (var id in body.DescendantNodes().OfType()) { - hasInitCall = true; - break; + if (id.Identifier.ValueText == "HotReloadInitialize") + { + hasInitCall = true; + break; + } } + + if (hasInitCall) + break; } return new HotReloadableClassInfo( From 994b44601442eeacff6a99de25fa891772f15f93 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Wed, 13 May 2026 08:54:46 +0200 Subject: [PATCH 7/7] align with dotnet/maui#35229: rename to IHotReloadAware, pass updated types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the API surface with the design tracked in: https://github.com/dotnet/maui/issues/35229 (Hot Reload Developer Experience epic) https://github.com/dotnet/maui/issues/35227 (Design IHotReloadAware interface) https://github.com/dotnet/maui/issues/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> --- ...enerator.cs => HotReloadAwareGenerator.cs} | 22 +++++++++---------- .../HotReloadRegistry.cs | 20 ++++++++--------- .../{IHotReloadable.cs => IHotReloadAware.cs} | 14 +++++++++--- .../MauiMetadataUpdateHandler.cs | 7 ++++-- 4 files changed, 37 insertions(+), 26 deletions(-) rename src/HotReload/Microsoft.Maui.HotReload.SourceGen/{HotReloadableGenerator.cs => HotReloadAwareGenerator.cs} (88%) rename src/HotReload/Microsoft.Maui.HotReload/{IHotReloadable.cs => IHotReloadAware.cs} (51%) diff --git a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadAwareGenerator.cs similarity index 88% rename from src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs rename to src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadAwareGenerator.cs index 98e54e2a5..59f1dd1b0 100644 --- a/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadableGenerator.cs +++ b/src/HotReload/Microsoft.Maui.HotReload.SourceGen/HotReloadAwareGenerator.cs @@ -12,22 +12,22 @@ namespace Microsoft.Maui.Labs.HotReload.SourceGen { /// /// Generates a HotReloadInitialize() method for every partial class that - /// implements Microsoft.Maui.Labs.HotReload.IHotReloadable. Also emits an informational + /// implements Microsoft.Maui.Labs.HotReload.IHotReloadAware. Also emits an informational /// diagnostic if no constructor appears to call it. /// [Generator] - public sealed class HotReloadableGenerator : IIncrementalGenerator + public sealed class HotReloadAwareGenerator : IIncrementalGenerator { - const string IHotReloadableFullName = "Microsoft.Maui.Labs.HotReload.IHotReloadable"; + const string IHotReloadAwareFullName = "Microsoft.Maui.Labs.HotReload.IHotReloadAware"; static readonly DiagnosticDescriptor MissingInitCallDescriptor = new( id: "MUH0001", title: "HotReloadInitialize not called", - messageFormat: "'{0}' implements IHotReloadable but no constructor calls HotReloadInitialize(). Add a call to HotReloadInitialize() in your constructor to enable hot reload notifications.", + messageFormat: "'{0}' implements IHotReloadAware but no constructor calls HotReloadInitialize(). Add a call to HotReloadInitialize() in your constructor to enable hot reload notifications.", category: "HotReload", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Types implementing IHotReloadable must call the generated HotReloadInitialize() in their constructor for hot reload registration to take effect."); + description: "Types implementing IHotReloadAware must call the generated HotReloadInitialize() in their constructor for hot reload registration to take effect."); public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -63,9 +63,9 @@ node is ClassDeclarationSyntax c && // Check interfaces: prefer semantic model, but fall back to syntax name matching // so the generator works even when the referenced assembly isn't fully loaded // in the compilation (e.g. net10.0 lib consumed by a net11.0-* project). - bool implementsIHotReloadable = false; + bool implementsIHotReloadAware = false; - var iface = ctx.SemanticModel.Compilation.GetTypeByMetadataName(IHotReloadableFullName); + var iface = ctx.SemanticModel.Compilation.GetTypeByMetadataName(IHotReloadAwareFullName); if (iface is not null) { // Semantic path: exact type identity check @@ -73,7 +73,7 @@ node is ClassDeclarationSyntax c && { if (SymbolEqualityComparer.Default.Equals(i, iface)) { - implementsIHotReloadable = true; + implementsIHotReloadAware = true; break; } } @@ -89,15 +89,15 @@ node is ClassDeclarationSyntax c && QualifiedNameSyntax q => q.Right.Identifier.Text, _ => null }; - if (typeName == "IHotReloadable") + if (typeName == "IHotReloadAware") { - implementsIHotReloadable = true; + implementsIHotReloadAware = true; break; } } } - if (!implementsIHotReloadable) + if (!implementsIHotReloadAware) return null; // Check whether any constructor in this partial declaration calls HotReloadInitialize. diff --git a/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs b/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs index f29353969..b41f7101c 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs +++ b/src/HotReload/Microsoft.Maui.HotReload/HotReloadRegistry.cs @@ -5,7 +5,7 @@ namespace Microsoft.Maui.Labs.HotReload { /// - /// Registry that tracks live instances so they can be notified + /// Registry that tracks live instances so they can be notified /// when their type is updated via .NET Hot Reload. /// /// @@ -14,13 +14,13 @@ namespace Microsoft.Maui.Labs.HotReload public static class HotReloadRegistry { static readonly object _lock = new(); - static readonly Dictionary>> _registry = new(); + static readonly Dictionary>> _registry = new(); /// - /// Registers so it receives + /// Registers so it receives /// callbacks when its type (or any base type) is updated at runtime. /// - public static void Register(IHotReloadable instance) + public static void Register(IHotReloadAware instance) { if (instance is null) return; @@ -29,11 +29,11 @@ public static void Register(IHotReloadable instance) lock (_lock) { if (!_registry.TryGetValue(type, out var list)) - _registry[type] = list = new List>(); + _registry[type] = list = new List>(); // Compact dead references before adding to keep the list small. list.RemoveAll(static w => !w.TryGetTarget(out _)); - list.Add(new WeakReference(instance)); + list.Add(new WeakReference(instance)); } } @@ -41,7 +41,7 @@ public static void Register(IHotReloadable instance) /// Removes from the registry. /// Safe to call even if the instance was never registered. /// - public static void Unregister(IHotReloadable instance) + public static void Unregister(IHotReloadAware instance) { if (instance is null) return; @@ -74,7 +74,7 @@ public static void Unregister(IHotReloadable instance) /// internal static void NotifyInstances(Type[] updatedTypes) { - List? toNotify = null; + List? toNotify = null; lock (_lock) { @@ -93,7 +93,7 @@ internal static void NotifyInstances(Type[] updatedTypes) // Deduplicate by reference to avoid double-notifying when both a base // type and derived type appear in updatedTypes simultaneously. - toNotify ??= new List(); + toNotify ??= new List(); bool alreadyQueued = false; for (int i = 0; i < toNotify.Count; i++) { @@ -117,7 +117,7 @@ internal static void NotifyInstances(Type[] updatedTypes) { try { - instance.OnHotReload(); + instance.OnHotReload(updatedTypes); } catch (Exception ex) { diff --git a/src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs b/src/HotReload/Microsoft.Maui.HotReload/IHotReloadAware.cs similarity index 51% rename from src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs rename to src/HotReload/Microsoft.Maui.HotReload/IHotReloadAware.cs index 4f7fbeeea..7d2cdf606 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/IHotReloadable.cs +++ b/src/HotReload/Microsoft.Maui.HotReload/IHotReloadAware.cs @@ -1,5 +1,7 @@ // #nullable enable +using System; + namespace Microsoft.Maui.Labs.HotReload { /// @@ -9,9 +11,15 @@ namespace Microsoft.Maui.Labs.HotReload /// To register for notifications, call HotReloadInitialize() in your constructor /// (generated automatically by Microsoft.Maui.Labs.HotReload.SourceGen for partial classes). /// - public interface IHotReloadable + public interface IHotReloadAware { - /// Called when this instance's type (or a base type) has been updated via hot reload. - void OnHotReload(); + /// + /// Called when this instance's type (or a base type) has been updated via hot reload. + /// + /// + /// The set of types that were updated, as reported by the .NET Hot Reload host. + /// May be if the host did not provide type information. + /// + void OnHotReload(Type[]? updatedTypes); } } diff --git a/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs b/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs index c548be23a..aaaca86b0 100644 --- a/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs +++ b/src/HotReload/Microsoft.Maui.HotReload/MauiMetadataUpdateHandler.cs @@ -1,17 +1,20 @@ #nullable enable using System; +#if !NETSTANDARD +using Microsoft.Maui.HotReload; +#endif namespace Microsoft.Maui.Labs.HotReload { /// /// Handles .NET Hot Reload metadata update notifications, forwarding them to registered - /// instances and the MAUI view-level hot reload infrastructure. + /// instances and the MAUI view-level hot reload infrastructure. /// public static class MauiMetadataUpdateHandler { /// /// Called by the .NET Hot Reload host after metadata has been applied. - /// Notifies all registered instances whose types were updated, + /// Notifies all registered instances whose types were updated, /// then forwards to MAUI's MauiHotReloadHelper for view-level reload. /// ///