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