diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs
index 1780a78a6a84..56cf2f3d6138 100644
--- a/src/Components/Components/src/ComponentBase.cs
+++ b/src/Components/Components/src/ComponentBase.cs
@@ -3,6 +3,7 @@
using System;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components
@@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Components
/// Optional base class for components. Alternatively, components may
/// implement directly.
///
- public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender
+ public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRender, IReceiveHotReloadContext
{
private readonly RenderFragment _renderFragment;
private RenderHandle _renderHandle;
@@ -29,6 +30,7 @@ public abstract class ComponentBase : IComponent, IHandleEvent, IHandleAfterRend
private bool _hasNeverRendered = true;
private bool _hasPendingQueuedRender;
private bool _hasCalledOnAfterRender;
+ private HotReloadContext? _hotReloadContext;
///
/// Constructs an instance of .
@@ -102,7 +104,7 @@ protected void StateHasChanged()
return;
}
- if (_hasNeverRendered || ShouldRender())
+ if (_hasNeverRendered || ShouldRender() || (_hotReloadContext?.IsHotReloading ?? false))
{
_hasPendingQueuedRender = true;
@@ -329,5 +331,10 @@ Task IHandleAfterRender.OnAfterRenderAsync()
// have to use "async void" and do their own exception handling in
// the case where they want to start an async task.
}
+
+ void IReceiveHotReloadContext.Receive(HotReloadContext context)
+ {
+ _hotReloadContext = context;
+ }
}
}
diff --git a/src/Components/Components/src/HotReload/HotReloadContext.cs b/src/Components/Components/src/HotReload/HotReloadContext.cs
new file mode 100644
index 000000000000..35948bdc6859
--- /dev/null
+++ b/src/Components/Components/src/HotReload/HotReloadContext.cs
@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Components.HotReload
+{
+ ///
+ /// A context that indicates when a component is being rendered because of a hot reload operation.
+ ///
+ public sealed class HotReloadContext
+ {
+ ///
+ /// Gets a value that indicates if the application is re-rendering in response to a hot-reload change.
+ ///
+ public bool IsHotReloading { get; internal set; }
+ }
+}
diff --git a/src/Components/Components/src/HotReload/HotReloadEnvironment.cs b/src/Components/Components/src/HotReload/HotReloadEnvironment.cs
new file mode 100644
index 000000000000..fef2cd9255cd
--- /dev/null
+++ b/src/Components/Components/src/HotReload/HotReloadEnvironment.cs
@@ -0,0 +1,22 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Components.HotReload
+{
+ internal class HotReloadEnvironment
+ {
+ public static readonly HotReloadEnvironment Instance = new(Environment.GetEnvironmentVariable("DOTNET_MODIFIABLE_ASSEMBLIES") == "debug");
+
+ public HotReloadEnvironment(bool isHotReloadEnabled)
+ {
+ IsHotReloadEnabled = isHotReloadEnabled;
+ }
+
+ ///
+ /// Gets a value that determines if HotReload is configured for this application.
+ ///
+ public bool IsHotReloadEnabled { get; }
+ }
+}
diff --git a/src/Components/Components/src/HotReload/HotReloadManager.cs b/src/Components/Components/src/HotReload/HotReloadManager.cs
new file mode 100644
index 000000000000..bcf59b7ea6b5
--- /dev/null
+++ b/src/Components/Components/src/HotReload/HotReloadManager.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Reflection;
+
+[assembly: AssemblyMetadata("ReceiveHotReloadDeltaNotification", "Microsoft.AspNetCore.Components.HotReload.HotReloadManager")]
+
+namespace Microsoft.AspNetCore.Components.HotReload
+{
+ internal static class HotReloadManager
+ {
+ internal static event Action? OnDeltaApplied;
+
+ public static void DeltaApplied()
+ {
+ OnDeltaApplied?.Invoke();
+ }
+ }
+}
diff --git a/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs b/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs
new file mode 100644
index 000000000000..8de60e382310
--- /dev/null
+++ b/src/Components/Components/src/HotReload/IReceiveHotReloadContext.cs
@@ -0,0 +1,17 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Components.HotReload
+{
+ ///
+ /// Allows a component to receive a .
+ ///
+ public interface IReceiveHotReloadContext : IComponent
+ {
+ ///
+ /// Configures a component to use the hot reload context.
+ ///
+ /// The hot reload context.
+ void Receive(HotReloadContext context);
+ }
+}
diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
index 2ee1e93ba030..3c4e0bc9a706 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
@@ -1,4 +1,4 @@
-
+$(DefaultNetCoreTargetFramework)
@@ -33,6 +33,10 @@
+
+
+
+
ToDictionary()
return result;
}
+ internal ParameterView Clone()
+ {
+ if (ReferenceEquals(_frames, _emptyFrames))
+ {
+ return Empty;
+ }
+
+ var numEntries = GetEntryCount();
+ var cloneBuffer = new RenderTreeFrame[1 + numEntries];
+ cloneBuffer[0] = RenderTreeFrame.PlaceholderChildComponentWithSubtreeLength(1 + numEntries);
+ _frames.AsSpan(1, numEntries).CopyTo(cloneBuffer.AsSpan(1));
+
+ return new ParameterView(Lifetime, cloneBuffer, _ownerIndex);
+ }
+
internal ParameterView WithCascadingParameters(IReadOnlyList cascadingParameters)
=> new ParameterView(_lifetime, _frames, _ownerIndex, cascadingParameters);
@@ -189,11 +204,7 @@ internal void CaptureSnapshot(ArrayBuilder builder)
{
builder.Clear();
- var numEntries = 0;
- foreach (var entry in this)
- {
- numEntries++;
- }
+ var numEntries = GetEntryCount();
// We need to prefix the captured frames with an "owner" frame that
// describes the length of the buffer so that ParameterView
@@ -207,6 +218,17 @@ internal void CaptureSnapshot(ArrayBuilder builder)
}
}
+ private int GetEntryCount()
+ {
+ var numEntries = 0;
+ foreach (var _ in this)
+ {
+ numEntries++;
+ }
+
+ return numEntries;
+ }
+
///
/// Creates a new from the given .
///
diff --git a/src/Components/Components/src/Properties/AssemblyInfo.cs b/src/Components/Components/src/Properties/AssemblyInfo.cs
index 8ff5dba8445c..66e087922b0d 100644
--- a/src/Components/Components/src/Properties/AssemblyInfo.cs
+++ b/src/Components/Components/src/Properties/AssemblyInfo.cs
@@ -8,5 +8,6 @@
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Components.TestServer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/Components/Components/src/Properties/ILLink.Substitutions.xml b/src/Components/Components/src/Properties/ILLink.Substitutions.xml
new file mode 100644
index 000000000000..edd5fb323df5
--- /dev/null
+++ b/src/Components/Components/src/Properties/ILLink.Substitutions.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index 3032b67cb877..ab4b3f271702 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -8,6 +8,11 @@ Microsoft.AspNetCore.Components.ComponentApplicationState.PersistAsJson(
Microsoft.AspNetCore.Components.ComponentApplicationState.PersistState(string! key, byte[]! value) -> void
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakeAsJson(string! key, out TValue? instance) -> bool
Microsoft.AspNetCore.Components.ComponentApplicationState.TryTakePersistedState(string! key, out byte[]? value) -> bool
+Microsoft.AspNetCore.Components.HotReload.HotReloadContext
+Microsoft.AspNetCore.Components.HotReload.HotReloadContext.HotReloadContext() -> void
+Microsoft.AspNetCore.Components.HotReload.HotReloadContext.IsHotReloading.get -> bool
+Microsoft.AspNetCore.Components.HotReload.IReceiveHotReloadContext
+Microsoft.AspNetCore.Components.HotReload.IReceiveHotReloadContext.Receive(Microsoft.AspNetCore.Components.HotReload.HotReloadContext! context) -> void
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.ComponentApplicationLifetime(Microsoft.Extensions.Logging.ILogger! logger) -> void
Microsoft.AspNetCore.Components.Lifetime.ComponentApplicationLifetime.PersistStateAsync(Microsoft.AspNetCore.Components.Lifetime.IComponentApplicationStateStore! store, Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer) -> System.Threading.Tasks.Task!
diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
index 80bf99724e0b..674684a65367 100644
--- a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
+++ b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs
@@ -5,8 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
-using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.RenderTree
@@ -535,15 +533,25 @@ private static void UpdateRetainedChildComponent(
// comparisons it wants with the old values. Later we could choose to pass the
// old parameter values if we wanted. By default, components always rerender
// after any SetParameters call, which is safe but now always optimal for perf.
+
+ // When performing hot reload, we want to force all components to re-render.
+ // We do this using two mechanisms - we call SetParametersAsync even if the parameters
+ // are unchanged and we ignore ComponentBase.ShouldRender
+
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))
+ if (!newParameters.DefinitelyEquals(oldParameters) || IsHotReloading(diffContext.Renderer))
{
componentState.SetDirectParameters(newParameters);
}
}
+ ///
+ /// Intentionally authored as a separate method so we can trim this code.
+ ///
+ private static bool IsHotReloading(Renderer renderer) => renderer.HotReloadContext.IsHotReloading;
+
private static int NextSiblingIndex(in RenderTreeFrame frame, int frameIndex)
{
switch (frame.FrameTypeField)
@@ -696,8 +704,8 @@ private static void AppendDiffEntriesForFramesWithSameSequence(
break;
}
- // We don't handle attributes here, they have their own diff logic.
- // See AppendDiffEntriesForAttributeFrame
+ // We don't handle attributes here, they have their own diff logic.
+ // See AppendDiffEntriesForAttributeFrame
default:
throw new NotImplementedException($"Encountered unsupported frame type during diffing: {newTree[newFrameIndex].FrameTypeField}");
}
diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs
index 38276aeaf426..0748d1675754 100644
--- a/src/Components/Components/src/RenderTree/Renderer.cs
+++ b/src/Components/Components/src/RenderTree/Renderer.cs
@@ -9,6 +9,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -32,13 +33,16 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
private readonly Dictionary _eventHandlerIdReplacements = new Dictionary();
private readonly ILogger _logger;
private readonly ComponentFactory _componentFactory;
+ private HotReloadContext _hotReloadContext;
+ private HotReloadEnvironment? _hotReloadEnvironment;
+ private List<(ComponentState, ParameterView)>? _rootComponents;
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
private bool _isBatchInProgress;
private ulong _lastEventHandlerId;
- private List _pendingTasks;
+ private List? _pendingTasks;
private bool _disposed;
- private Task _disposeTask;
+ private Task? _disposeTask;
///
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
@@ -92,6 +96,22 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
_serviceProvider = serviceProvider;
_logger = loggerFactory.CreateLogger();
_componentFactory = new ComponentFactory(componentActivator);
+
+ InitializeHotReload(serviceProvider);
+ }
+
+ private void InitializeHotReload(IServiceProvider serviceProvider)
+ {
+ _hotReloadContext = new();
+
+ // HotReloadEnvironment is a test-specific feature and may not be available in a running app. We'll fallback to the default instance
+ // if the test fixture does not provide one.
+ _hotReloadEnvironment = serviceProvider.GetService() ?? HotReloadEnvironment.Instance;
+
+ if (_hotReloadEnvironment.IsHotReloadEnabled)
+ {
+ HotReloadManager.OnDeltaApplied += RenderRootComponentsOnHotReload;
+ }
}
private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvider serviceProvider)
@@ -101,7 +121,7 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
}
///
- /// Gets the associated with this .
+ /// Gets the associated with this .
///
public abstract Dispatcher Dispatcher { get; }
@@ -111,13 +131,54 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid
///
protected internal ElementReferenceContext? ElementReferenceContext { get; protected set; }
+ internal HotReloadContext HotReloadContext => _hotReloadContext;
+
+ private async void RenderRootComponentsOnHotReload()
+ {
+ await Dispatcher.InvokeAsync(() =>
+ {
+ if (_rootComponents is null)
+ {
+ return;
+ }
+
+ HotReloadContext.IsHotReloading = true;
+ try
+ {
+ foreach (var (componentState, initialParameters) in _rootComponents)
+ {
+ componentState.SetDirectParameters(initialParameters);
+ }
+ }
+ finally
+ {
+ HotReloadContext.IsHotReloading = false;
+ }
+ });
+ }
+
///
/// Constructs a new component of the specified type.
///
/// The type of the component to instantiate.
/// The component instance.
protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType)
- => _componentFactory.InstantiateComponent(_serviceProvider, componentType);
+ {
+ var component = _componentFactory.InstantiateComponent(_serviceProvider, componentType);
+ InstatiateComponentForHotReload(component);
+ return component;
+ }
+
+ ///
+ /// Intentionally authored as a separate method call so we can trim this code.
+ ///
+ private void InstatiateComponentForHotReload(IComponent component)
+ {
+ if (_hotReloadEnvironment is not null && _hotReloadEnvironment.IsHotReloadEnabled && component is IReceiveHotReloadContext receiveHotReloadContext)
+ {
+ receiveHotReloadContext.Receive(HotReloadContext);
+ }
+ }
///
/// Associates the with the , assigning
@@ -182,6 +243,8 @@ protected async Task RenderRootComponentAsync(int componentId, ParameterView ini
// During the asynchronous rendering process we want to wait up until all components have
// finished rendering so that we can produce the complete output.
var componentState = GetRequiredComponentState(componentId);
+ CaptureRootComponentForHotReload(initialParameters, componentState);
+
componentState.SetDirectParameters(initialParameters);
try
@@ -195,6 +258,20 @@ protected async Task RenderRootComponentAsync(int componentId, ParameterView ini
}
}
+ ///
+ /// Intentionally authored as a separate method call so we can trim this code.
+ ///
+ private void CaptureRootComponentForHotReload(ParameterView initialParameters, ComponentState componentState)
+ {
+ if (_hotReloadEnvironment?.IsHotReloadEnabled ?? false)
+ {
+ // 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.
+ _rootComponents ??= new();
+ _rootComponents.Add((componentState, initialParameters.Clone()));
+ }
+ }
+
///
/// Allows derived types to handle exceptions during rendering. Defaults to rethrowing the original exception.
///
@@ -260,7 +337,7 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
UpdateRenderTreeToMatchClientState(latestEquivalentEventHandlerId, fieldInfo);
}
- Task task = null;
+ Task? task = null;
try
{
// The event handler might request multiple renders in sequence. Capture them
@@ -884,6 +961,8 @@ public void Dispose()
///
public async ValueTask DisposeAsync()
{
+ DisposeForHotReload();
+
if (_disposed)
{
return;
@@ -906,5 +985,16 @@ public async ValueTask DisposeAsync()
}
}
}
+
+ ///
+ /// Intentionally authored as a separate method call so we can trim this code.
+ ///
+ private void DisposeForHotReload()
+ {
+ if (_hotReloadEnvironment?.IsHotReloadEnabled ?? false)
+ {
+ HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload;
+ }
+ }
}
}
diff --git a/src/Components/Components/test/ParameterViewTest.cs b/src/Components/Components/test/ParameterViewTest.cs
index 5193e8fe296c..6cf26a7a7ee3 100644
--- a/src/Components/Components/test/ParameterViewTest.cs
+++ b/src/Components/Components/test/ParameterViewTest.cs
@@ -348,6 +348,67 @@ public void CannotReadAfterLifetimeExpiry()
Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
}
+ [Fact]
+ public void Clone_EmptyParameterView()
+ {
+ // Arrange
+ var initial = ParameterView.Empty;
+
+ // Act
+ var cloned = initial.Clone();
+
+ // Assert
+ Assert.Empty(ToEnumerable(cloned));
+ }
+
+ [Fact]
+ public void Clone_ParameterViewSingleParameter()
+ {
+ // Arrange
+ var attribute1Value = new object();
+ var initial = new ParameterView(ParameterViewLifetime.Unbound, new[]
+ {
+ RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(2),
+ RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value),
+ }, 0);
+
+
+ // Act
+ var cloned = initial.Clone();
+
+ // Assert
+ Assert.Collection(
+ ToEnumerable(cloned),
+ p => AssertParameter("attribute 1", attribute1Value, expectedIsCascading: false));
+ }
+
+ [Fact]
+ public void Clone_ParameterPreservesOrder()
+ {
+ // Arrange
+ var attribute1Value = new object();
+ var attribute2Value = new object();
+ var attribute3Value = new object();
+ var initial = new ParameterView(ParameterViewLifetime.Unbound, new[]
+ {
+ RenderTreeFrame.Element(0, "some element").WithElementSubtreeLength(4),
+ RenderTreeFrame.Attribute(1, "attribute 1", attribute1Value),
+ RenderTreeFrame.Attribute(1, "attribute 2", attribute2Value),
+ RenderTreeFrame.Attribute(1, "attribute 3", attribute3Value),
+ }, 0);
+
+
+ // Act
+ var cloned = initial.Clone();
+
+ // Assert
+ Assert.Collection(
+ ToEnumerable(cloned),
+ p => AssertParameter("attribute 1", attribute1Value, expectedIsCascading: false),
+ p => AssertParameter("attribute 2", attribute2Value, expectedIsCascading: false),
+ p => AssertParameter("attribute 3", attribute3Value, expectedIsCascading: false));
+ }
+
private Action AssertParameter(string expectedName, object expectedValue, bool expectedIsCascading)
{
return parameter =>
diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.ts b/src/Components/Web.JS/src/Boot.WebAssembly.ts
index 1fa5f26023c6..5fd2f2bffc46 100644
--- a/src/Components/Web.JS/src/Boot.WebAssembly.ts
+++ b/src/Components/Web.JS/src/Boot.WebAssembly.ts
@@ -38,6 +38,10 @@ async function boot(options?: Partial): Promise {
Blazor._internal.InputFile = WasmInputFile;
+ Blazor._internal.applyHotReload = (id: string, metadataDelta: string, ilDeta: string) => {
+ DotNet.invokeMethod('Microsoft.AspNetCore.Components.WebAssembly', 'ApplyHotReloadDelta', id, metadataDelta, ilDeta);
+ };
+
// Configure JS interop
Blazor._internal.invokeJSFromDotNet = invokeJSFromDotNet;
diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts
index 7cc3c3747280..43af8414820b 100644
--- a/src/Components/Web.JS/src/GlobalExports.ts
+++ b/src/Components/Web.JS/src/GlobalExports.ts
@@ -45,7 +45,8 @@ interface IBlazor {
readSatelliteAssemblies?: () => System_Array,
getLazyAssemblies?: any
dotNetCriticalError?: any
- getSatelliteAssemblies?: any
+ getSatelliteAssemblies?: any,
+ applyHotReload?: (id: string, metadataDelta: string, ilDelta: string) => void
}
}
@@ -61,4 +62,4 @@ export const Blazor: IBlazor = {
};
// Make the following APIs available in global scope for invocation from JS
-window['Blazor'] = Blazor;
\ No newline at end of file
+window['Blazor'] = Blazor;
diff --git a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs
index c6eb32b09229..c4c486c8e17b 100644
--- a/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs
+++ b/src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs
@@ -1,8 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Rendering;
@@ -40,5 +44,23 @@ public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string even
webEvent.EventFieldInfo,
webEvent.EventArgs);
}
+
+ ///
+ /// For framework use only.
+ ///
+ [JSInvokable(nameof(ApplyHotReloadDelta))]
+ public static void ApplyHotReloadDelta(string moduleId, byte[] metadataDelta, byte[] ilDeta)
+ {
+ var moduleIdGuid = Guid.Parse(moduleId);
+ var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.Modules.FirstOrDefault() is Module m && m.ModuleVersionId == moduleIdGuid);
+
+ if (assembly is not null)
+ {
+ System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate(assembly, metadataDelta, ilDeta, ReadOnlySpan.Empty);
+ }
+
+ // Remove this once there's a runtime API to subscribe to.
+ typeof(ComponentBase).Assembly.GetType("Microsoft.AspNetCore.Components.HotReload.HotReloadManager")!.GetMethod("DeltaApplied", BindingFlags.NonPublic | BindingFlags.Static)!.Invoke(null, null);
+ }
}
}
diff --git a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml
index 732104ae1516..986fa0232bcd 100644
--- a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml
+++ b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.WarningSuppressions.xml
@@ -19,6 +19,12 @@
memberM:Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder.InitializeRegisteredRootComponents(Microsoft.JSInterop.IJSUnmarshalledRuntime)
+
+ ILLink
+ IL2026
+ member
+ M:Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[])
+ ILLinkIL2026
@@ -43,5 +49,11 @@
memberM:Microsoft.AspNetCore.Components.WebAssemblyComponentParameterDeserializer.DeserializeParameters(System.Collections.Generic.IList{Microsoft.AspNetCore.Components.ComponentParameter},System.Collections.Generic.IList{System.Object})
+
+ ILLink
+ IL2075
+ member
+ M:Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.ApplyHotReloadDelta(System.String,System.Byte[],System.Byte[])
+
\ No newline at end of file
diff --git a/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt b/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt
index 1de9e94bc802..bcdda6e1d927 100644
--- a/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt
+++ b/src/Components/WebAssembly/WebAssembly/src/PublicAPI.Unshipped.txt
@@ -38,5 +38,6 @@ static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMe
static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserRequestMode(this System.Net.Http.HttpRequestMessage! requestMessage, Microsoft.AspNetCore.Components.WebAssembly.Http.BrowserRequestMode requestMode) -> System.Net.Http.HttpRequestMessage!
static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserRequestOption(this System.Net.Http.HttpRequestMessage! requestMessage, string! name, object! value) -> System.Net.Http.HttpRequestMessage!
static Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserResponseStreamingEnabled(this System.Net.Http.HttpRequestMessage! requestMessage, bool streamingEnabled) -> System.Net.Http.HttpRequestMessage!
+static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.ApplyHotReloadDelta(string! moduleId, byte[]! metadataDelta, byte[]! ilDeta) -> void
static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.DispatchEvent(Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor! eventDescriptor, string! eventArgsJson) -> System.Threading.Tasks.Task!
static Microsoft.AspNetCore.Components.WebAssembly.Infrastructure.JSInteropMethods.NotifyLocationChanged(string! uri, bool isInterceptedLink) -> void
diff --git a/src/Components/test/E2ETest/ServerExecutionTests/HotReloadTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/HotReloadTest.cs
new file mode 100644
index 000000000000..e813e232b83b
--- /dev/null
+++ b/src/Components/test/E2ETest/ServerExecutionTests/HotReloadTest.cs
@@ -0,0 +1,55 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using BasicTestApp.HotReload;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
+using Microsoft.AspNetCore.E2ETesting;
+using OpenQA.Selenium;
+using TestServer;
+using Xunit;
+using Xunit.Abstractions;
+using System.Net.Http;
+
+namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
+{
+ public class HotReloadTest : ServerTestBase>
+ {
+ public HotReloadTest(
+ BrowserFixture browserFixture,
+ BasicTestAppServerSiteFixture serverFixture,
+ ITestOutputHelper output)
+ : base(browserFixture, serverFixture, output)
+ {
+ }
+
+ public override async Task InitializeAsync()
+ {
+ await base.InitializeAsync(Guid.NewGuid().ToString());
+ }
+
+ protected override void InitializeAsyncCore()
+ {
+ Navigate(ServerPathBase, noReload: false);
+ Browser.MountTestComponent();
+ }
+
+ [Fact]
+ public async Task InvokingRender_CausesComponentToRender()
+ {
+ Browser.Equal("This component was rendered 1 time(s).", () => Browser.Exists(By.TagName("h2")).Text);
+ Browser.Equal("Initial title", () => Browser.Exists(By.TagName("h3")).Text);
+ Browser.Equal("Component with ShouldRender=false was rendered 1 time(s).", () => Browser.Exists(By.TagName("h4")).Text);
+
+ using var client = new HttpClient { BaseAddress = _serverFixture.RootUri };
+ var response = await client.GetAsync("/rerender");
+ response.EnsureSuccessStatusCode();
+
+ Browser.Equal("This component was rendered 2 time(s).", () => Browser.Exists(By.TagName("h2")).Text);
+ Browser.Equal("Initial title", () => Browser.Exists(By.TagName("h3")).Text);
+ Browser.Equal("Component with ShouldRender=false was rendered 2 time(s).", () => Browser.Exists(By.TagName("h4")).Text);
+ }
+ }
+}
diff --git a/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithParameter.razor b/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithParameter.razor
new file mode 100644
index 000000000000..ee99fbdb6db3
--- /dev/null
+++ b/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithParameter.razor
@@ -0,0 +1,6 @@
+
@Title
+
+@code
+{
+ [Parameter] public string Title { get; set; }
+}
\ No newline at end of file
diff --git a/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithShouldRender.razor b/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithShouldRender.razor
new file mode 100644
index 000000000000..1cea3335f9da
--- /dev/null
+++ b/src/Components/test/testassets/BasicTestApp/HotReload/ComponentWithShouldRender.razor
@@ -0,0 +1,8 @@
+
Component with ShouldRender=false was rendered @(++RenderCount) time(s).
+
+@code
+{
+ int RenderCount = 0;
+
+ protected override bool ShouldRender() => false;
+}
\ No newline at end of file
diff --git a/src/Components/test/testassets/BasicTestApp/HotReload/RenderOnHotReload.razor b/src/Components/test/testassets/BasicTestApp/HotReload/RenderOnHotReload.razor
new file mode 100644
index 000000000000..00f35e56c292
--- /dev/null
+++ b/src/Components/test/testassets/BasicTestApp/HotReload/RenderOnHotReload.razor
@@ -0,0 +1,10 @@
+
This component was rendered @(++RenderCount) time(s).
+
+
+
+
+
+@code
+{
+ int RenderCount = 0;
+}
\ No newline at end of file
diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor
index ba51153b4733..34a05f91dac4 100644
--- a/src/Components/test/testassets/BasicTestApp/Index.razor
+++ b/src/Components/test/testassets/BasicTestApp/Index.razor
@@ -92,6 +92,7 @@
+
@System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription
diff --git a/src/Components/test/testassets/TestServer/Controllers/ReloadController.cs b/src/Components/test/testassets/TestServer/Controllers/ReloadController.cs
new file mode 100644
index 000000000000..162f4540099d
--- /dev/null
+++ b/src/Components/test/testassets/TestServer/Controllers/ReloadController.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Components.HotReload;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ComponentsApp.Server
+{
+ [ApiController]
+ public class ReloadController : ControllerBase
+ {
+ [HttpGet("/rerender")]
+ public IActionResult Rerender()
+ {
+ HotReloadManager.DeltaApplied();
+
+ return Ok();
+ }
+ }
+}
diff --git a/src/Components/test/testassets/TestServer/HotReloadStartup.cs b/src/Components/test/testassets/TestServer/HotReloadStartup.cs
new file mode 100644
index 000000000000..f9b0061c9d41
--- /dev/null
+++ b/src/Components/test/testassets/TestServer/HotReloadStartup.cs
@@ -0,0 +1,45 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Globalization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Components.HotReload;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace TestServer
+{
+ public class HotReloadStartup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSingleton(new HotReloadEnvironment(isHotReloadEnabled: true));
+ services.AddControllers();
+ services.AddRazorPages();
+ services.AddServerSideBlazor();
+ }
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ var enUs = new CultureInfo("en-US");
+ CultureInfo.DefaultThreadCurrentCulture = enUs;
+ CultureInfo.DefaultThreadCurrentUICulture = enUs;
+
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ app.UseBlazorFrameworkFiles();
+ app.UseStaticFiles();
+ app.UseRouting();
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ endpoints.MapBlazorHub();
+ endpoints.MapFallbackToPage("/_ServerHost");
+ });
+ }
+ }
+}
diff --git a/src/Components/test/testassets/TestServer/Program.cs b/src/Components/test/testassets/TestServer/Program.cs
index 6351ada5b656..df8993c4c28d 100644
--- a/src/Components/test/testassets/TestServer/Program.cs
+++ b/src/Components/test/testassets/TestServer/Program.cs
@@ -29,6 +29,7 @@ public static async Task Main(string[] args)
["Globalization + Localization (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"),
["Server-side blazor"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"),
["Hosted client-side blazor"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"),
+ ["Hot Reload"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"),
["Dev server client-side blazor"] = CreateDevServerHost(CreateAdditionalArgs(args))
};