diff --git a/src/Components/Components/src/HotReload/HotReloadManager.cs b/src/Components/Components/src/HotReload/HotReloadManager.cs
index 3361cf4e25a8..7da893a07642 100644
--- a/src/Components/Components/src/HotReload/HotReloadManager.cs
+++ b/src/Components/Components/src/HotReload/HotReloadManager.cs
@@ -8,18 +8,22 @@
namespace Microsoft.AspNetCore.Components.HotReload
{
- internal static class HotReloadManager
+ internal sealed class HotReloadManager
{
- public static event Action? OnDeltaApplied;
+ public static readonly HotReloadManager Default = new();
+
+ public bool MetadataUpdateSupported { get; set; } = MetadataUpdater.IsSupported;
///
/// Gets a value that determines if OnDeltaApplied is subscribed to.
///
- public static bool IsSubscribedTo => OnDeltaApplied is not null;
+ public bool IsSubscribedTo => OnDeltaApplied is not null;
+
+ public event Action? OnDeltaApplied;
///
/// MetadataUpdateHandler event. This is invoked by the hot reload host via reflection.
///
- public static void UpdateApplication(Type[]? _) => OnDeltaApplied?.Invoke();
+ public static void UpdateApplication(Type[]? _) => Default.OnDeltaApplied?.Invoke();
}
}
diff --git a/src/Components/Components/src/HotReload/TestableMetadataUpdate.cs b/src/Components/Components/src/HotReload/TestableMetadataUpdate.cs
deleted file mode 100644
index 56a591235588..000000000000
--- a/src/Components/Components/src/HotReload/TestableMetadataUpdate.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Reflection.Metadata;
-
-namespace Microsoft.AspNetCore.Components.HotReload
-{
- internal sealed class TestableMetadataUpdate
- {
- public static bool TestIsSupported { private get; set; }
-
- ///
- /// A proxy for that is testable.
- ///
- public static bool IsSupported => MetadataUpdater.IsSupported || TestIsSupported;
- }
-}
diff --git a/src/Components/Components/src/Properties/ILLink.Substitutions.xml b/src/Components/Components/src/Properties/ILLink.Substitutions.xml
index 9f780bc2f8b2..1b0960153a8b 100644
--- a/src/Components/Components/src/Properties/ILLink.Substitutions.xml
+++ b/src/Components/Components/src/Properties/ILLink.Substitutions.xml
@@ -1,7 +1,7 @@
-
-
+
+
diff --git a/src/Components/Components/src/RenderHandle.cs b/src/Components/Components/src/RenderHandle.cs
index e7923ce41d0b..d87c9ce07b7b 100644
--- a/src/Components/Components/src/RenderHandle.cs
+++ b/src/Components/Components/src/RenderHandle.cs
@@ -47,7 +47,7 @@ public Dispatcher Dispatcher
///
/// Gets a value that determines if the is triggering a render in response to a metadata update (hot-reload) change.
///
- public bool IsRenderingOnMetadataUpdate => TestableMetadataUpdate.IsSupported && (_renderer?.IsRenderingOnMetadataUpdate ?? false);
+ public bool IsRenderingOnMetadataUpdate => HotReloadManager.Default.MetadataUpdateSupported && (_renderer?.IsRenderingOnMetadataUpdate ?? false);
internal bool IsRendererDisposed => _renderer?.Disposed
?? throw new InvalidOperationException("No renderer has been initialized.");
diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
index b5716c7ce3d6..99994a84273f 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
@@ -542,7 +542,7 @@ private static void UpdateRetainedChildComponent(
var oldParameters = new ParameterView(ParameterViewLifetime.Unbound, oldTree, oldComponentIndex);
var newParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
var newParameters = new ParameterView(newParametersLifetime, newTree, newComponentIndex);
- if (!newParameters.DefinitelyEquals(oldParameters) || (TestableMetadataUpdate.IsSupported && diffContext.Renderer.IsRenderingOnMetadataUpdate))
+ if (!newParameters.DefinitelyEquals(oldParameters) || (HotReloadManager.Default.MetadataUpdateSupported && diffContext.Renderer.IsRenderingOnMetadataUpdate))
{
componentState.SetDirectParameters(newParameters);
}
diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs
index ed3bc5951b65..351ba6aa1217 100644
--- a/src/Components/Components/src/RenderTree/Renderer.cs
+++ b/src/Components/Components/src/RenderTree/Renderer.cs
@@ -3,12 +3,8 @@
#nullable disable warnings
-using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using System.Threading;
-using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Reflection;
using Microsoft.AspNetCore.Components.Rendering;
@@ -45,6 +41,8 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
private Task? _disposeTask;
private bool _rendererIsDisposed;
+ private bool _hotReloadInitialized;
+
///
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
///
@@ -97,13 +95,11 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
_serviceProvider = serviceProvider;
_logger = loggerFactory.CreateLogger();
_componentFactory = new ComponentFactory(componentActivator);
-
- if (TestableMetadataUpdate.IsSupported)
- {
- HotReloadManager.OnDeltaApplied += RenderRootComponentsOnHotReload;
- }
}
+ internal HotReloadManager HotReloadManager { get; set; } = HotReloadManager.Default;
+
+
private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvider serviceProvider)
{
return serviceProvider.GetService()
@@ -179,7 +175,18 @@ protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)
/// The component's assigned identifier.
// Internal for unit testing
protected internal int AssignRootComponentId(IComponent component)
- => AttachAndInitComponent(component, -1).ComponentId;
+ {
+ if (!_hotReloadInitialized)
+ {
+ _hotReloadInitialized = true;
+ if (HotReloadManager.MetadataUpdateSupported)
+ {
+ HotReloadManager.OnDeltaApplied += RenderRootComponentsOnHotReload;
+ }
+ }
+
+ return AttachAndInitComponent(component, -1).ComponentId;
+ }
///
/// Gets the current render tree for a given component.
@@ -232,7 +239,7 @@ protected internal async Task RenderRootComponentAsync(int componentId, Paramete
_pendingTasks ??= new();
var componentState = GetRequiredRootComponentState(componentId);
- if (TestableMetadataUpdate.IsSupported)
+ if (HotReloadManager.MetadataUpdateSupported)
{
// When we're doing hot-reload, stash away the parameters used while rendering root components.
// We'll use this to trigger re-renders on hot reload updates.
@@ -262,7 +269,7 @@ protected internal void RemoveRootComponent(int componentId)
// Currently there's no known scenario where we need to support calling RemoveRootComponentAsync
// during a batch, but if a scenario emerges we can add support.
_batchBuilder.ComponentDisposalQueue.Enqueue(componentId);
- if (TestableMetadataUpdate.IsSupported)
+ if (HotReloadManager.MetadataUpdateSupported)
{
_rootComponentsLatestParameters?.Remove(componentId);
}
@@ -988,7 +995,7 @@ protected virtual void Dispose(bool disposing)
_rendererIsDisposed = true;
- if (TestableMetadataUpdate.IsSupported)
+ if (_hotReloadInitialized && HotReloadManager.MetadataUpdateSupported)
{
HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload;
}
diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs
index fdf28a973592..c48afc1d40ec 100644
--- a/src/Components/Components/src/Routing/Router.cs
+++ b/src/Components/Components/src/Routing/Router.cs
@@ -96,9 +96,9 @@ public void Attach(RenderHandle renderHandle)
_locationAbsolute = NavigationManager.Uri;
NavigationManager.LocationChanged += OnLocationChanged;
- if (TestableMetadataUpdate.IsSupported)
+ if (HotReloadManager.Default.MetadataUpdateSupported)
{
- HotReloadManager.OnDeltaApplied += ClearRouteCaches;
+ HotReloadManager.Default.OnDeltaApplied += ClearRouteCaches;
}
}
@@ -140,9 +140,9 @@ public async Task SetParametersAsync(ParameterView parameters)
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
- if (TestableMetadataUpdate.IsSupported)
+ if (HotReloadManager.Default.MetadataUpdateSupported)
{
- HotReloadManager.OnDeltaApplied -= ClearRouteCaches;
+ HotReloadManager.Default.OnDeltaApplied -= ClearRouteCaches;
}
}
diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs
index a2b5288acc20..4923bd31acfa 100644
--- a/src/Components/Components/test/RendererTest.cs
+++ b/src/Components/Components/test/RendererTest.cs
@@ -10,6 +10,7 @@
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
@@ -4733,6 +4734,53 @@ public async Task DisposeAsyncCallsComponentDisposeAsyncOnSyncContext()
Assert.True(wasOnSyncContext);
}
+ [Fact]
+ public async Task NoHotReloadListenersAreRegistered_WhenMetadataUpdatesAreNotSupported()
+ {
+ // Arrange
+ await using var renderer = new TestRenderer();
+ var hotReloadManager = new HotReloadManager { MetadataUpdateSupported = false };
+ renderer.HotReloadManager = hotReloadManager;
+ var component = new TestComponent(builder =>
+ {
+ builder.OpenElement(0, "h2");
+ builder.AddContent(1, "some text");
+ builder.CloseElement();
+ });
+
+ // Act
+ var componentId = renderer.AssignRootComponentId(component);
+ component.TriggerRender();
+ Assert.False(hotReloadManager.IsSubscribedTo);
+
+ await renderer.DisposeAsync();
+ }
+
+ [Fact]
+ public async Task DisposingRenderer_UnsubsribesFromHotReloadManager()
+ {
+ // Arrange
+ var renderer = new TestRenderer();
+ var hotReloadManager = new HotReloadManager { MetadataUpdateSupported = true };
+ renderer.HotReloadManager = hotReloadManager;
+ var component = new TestComponent(builder =>
+ {
+ builder.OpenElement(0, "h2");
+ builder.AddContent(1, "some text");
+ builder.CloseElement();
+ });
+
+ // Act
+ var componentId = renderer.AssignRootComponentId(component);
+ component.TriggerRender();
+ Assert.True(hotReloadManager.IsSubscribedTo);
+
+ await renderer.DisposeAsync();
+
+ // Assert
+ Assert.False(hotReloadManager.IsSubscribedTo);
+ }
+
private class TestComponentActivator : IComponentActivator where TResult : IComponent, new()
{
public List RequestedComponentTypes { get; } = new List();
diff --git a/src/Components/Web/src/JSComponents/JSComponentInterop.cs b/src/Components/Web/src/JSComponents/JSComponentInterop.cs
index 1418ea5ab431..9b0532310261 100644
--- a/src/Components/Web/src/JSComponents/JSComponentInterop.cs
+++ b/src/Components/Web/src/JSComponents/JSComponentInterop.cs
@@ -28,9 +28,9 @@ public class JSComponentInterop
static JSComponentInterop()
{
- if (MetadataUpdater.IsSupported)
+ if (HotReloadManager.Default.MetadataUpdateSupported)
{
- HotReloadManager.OnDeltaApplied += () => ParameterTypeCaches.Clear();
+ HotReloadManager.Default.OnDeltaApplied += () => ParameterTypeCaches.Clear();
}
}
diff --git a/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs
index 36ca4f14f55b..e7931103bb99 100644
--- a/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs
+++ b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs
@@ -135,28 +135,6 @@ public void CanAccessAuthenticationStateDuringStaticPrerendering(string initialU
Browser.Equal($"Hello, {interactiveUsername ?? "anonymous"}!", () => Browser.Exists(By.TagName("h1")).Text);
}
- [Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/35449")]
- public async Task NoHotReloadListenersAreOrdinarilyRegistered()
- {
- Navigate("/prerendered/prerendered-transition");
-
- // Prerendered output shows "not connected"
- Browser.Equal("not connected", () => Browser.Exists(By.Id("connected-state")).Text);
-
- // Once connected, output changes
- BeginInteractivity();
- Browser.Equal("connected", () => Browser.Exists(By.Id("connected-state")).Text);
-
- // Once connected, output changes
- BeginInteractivity();
- Browser.Equal("connected", () => Browser.Exists(By.Id("connected-state")).Text);
-
- // Now query the hot reload manager and verify nothing is still wired up by default.
- var httpClient = new HttpClient { BaseAddress = _serverFixture.RootUri };
- var hasEventHandlers = await httpClient.GetFromJsonAsync("/prerendered/ishotreloadsubscribedto");
- Assert.False(hasEventHandlers);
- }
-
private void BeginInteractivity()
{
Browser.Exists(By.Id("load-boot-script")).Click();
diff --git a/src/Components/test/testassets/TestServer/HotReloadStartup.cs b/src/Components/test/testassets/TestServer/HotReloadStartup.cs
index d9dcf184c783..fe7fa6c5341e 100644
--- a/src/Components/test/testassets/TestServer/HotReloadStartup.cs
+++ b/src/Components/test/testassets/TestServer/HotReloadStartup.cs
@@ -14,7 +14,7 @@ public class HotReloadStartup
{
public HotReloadStartup()
{
- TestableMetadataUpdate.TestIsSupported = true;
+ HotReloadManager.Default.MetadataUpdateSupported = true;
}
public void ConfigureServices(IServiceCollection services)
diff --git a/src/Components/test/testassets/TestServer/PrerenderedStartup.cs b/src/Components/test/testassets/TestServer/PrerenderedStartup.cs
index 8b2339fa9468..490805f7c8a7 100644
--- a/src/Components/test/testassets/TestServer/PrerenderedStartup.cs
+++ b/src/Components/test/testassets/TestServer/PrerenderedStartup.cs
@@ -47,8 +47,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseRouting();
app.UseEndpoints(endpoints =>
{
- endpoints.MapGet("ishotreloadsubscribedto", () => HotReloadManager.IsSubscribedTo);
-
endpoints.MapRazorPages();
endpoints.MapFallbackToPage("/PrerenderedHost");
endpoints.MapBlazorHub();