diff --git a/src/Components/Components/src/ComponentFactory.cs b/src/Components/Components/src/ComponentFactory.cs index 49b3a6a95b3a..c3235a2be405 100644 --- a/src/Components/Components/src/ComponentFactory.cs +++ b/src/Components/Components/src/ComponentFactory.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Components { - internal class ComponentFactory + internal sealed class ComponentFactory { private static readonly BindingFlags _injectablePropertyBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; @@ -25,6 +25,8 @@ public ComponentFactory(IComponentActivator componentActivator) _componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator)); } + public void ClearCache() => _cachedInitializers.Clear(); + public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType) { var component = _componentActivator.CreateInstance(componentType); diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml b/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml index 7a1510c2300d..cc79f6d37717 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.WarningSuppressions.xml @@ -17,7 +17,7 @@ ILLink IL2026 member - M:Microsoft.AspNetCore.Components.RouteTableFactory.<GetRouteableComponents>g__GetRouteableComponents|3_0(System.Collections.Generic.List{System.Type},System.Reflection.Assembly) + M:Microsoft.AspNetCore.Components.RouteTableFactory.<GetRouteableComponents>g__GetRouteableComponents|4_0(System.Collections.Generic.List{System.Type},System.Reflection.Assembly) ILLink diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index bab5d9d62957..0f45353abc63 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -20,6 +20,8 @@ internal static class ComponentProperties private readonly static ConcurrentDictionary _cachedWritersByType = new ConcurrentDictionary(); + public static void ClearCache() => _cachedWritersByType.Clear(); + public static void SetProperties(in ParameterView parameters, object target) { if (target == null) diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index b059b1998f20..fdc9b32d6210 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.HotReload; +using Microsoft.AspNetCore.Components.Reflection; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -126,6 +127,10 @@ private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvid private async void RenderRootComponentsOnHotReload() { + // Before re-rendering the root component, also clear any well-known caches in the framework + _componentFactory.ClearCache(); + ComponentProperties.ClearCache(); + await Dispatcher.InvokeAsync(() => { if (_rootComponents is null) diff --git a/src/Components/Components/src/Routing/RouteTableFactory.cs b/src/Components/Components/src/Routing/RouteTableFactory.cs index e2ecd0edb7b1..e8f263fd08b8 100644 --- a/src/Components/Components/src/Routing/RouteTableFactory.cs +++ b/src/Components/Components/src/Routing/RouteTableFactory.cs @@ -30,6 +30,8 @@ public static RouteTable Create(RouteKey routeKey) return routeTable; } + public static void ClearCaches() => Cache.Clear(); + private static List GetRouteableComponents(RouteKey routeKey) { var routeableComponents = new List(); diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 320403015858..e82af103b3da 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -9,6 +9,7 @@ using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.HotReload; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Routing @@ -33,7 +34,7 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary private Task _previousOnNavigateTask = Task.CompletedTask; - private RouteKey _currentRouteKey; + private RouteKey _routeTableLastBuiltForRouteKey; private bool _onNavigateCalled = false; @@ -91,6 +92,11 @@ public void Attach(RenderHandle renderHandle) _baseUri = NavigationManager.BaseUri; _locationAbsolute = NavigationManager.Uri; NavigationManager.LocationChanged += OnLocationChanged; + + if (HotReloadFeature.IsSupported) + { + HotReloadManager.OnDeltaApplied += ClearRouteCaches; + } } /// @@ -131,6 +137,10 @@ public async Task SetParametersAsync(ParameterView parameters) public void Dispose() { NavigationManager.LocationChanged -= OnLocationChanged; + if (HotReloadFeature.IsSupported) + { + HotReloadManager.OnDeltaApplied -= ClearRouteCaches; + } } private static string StringUntilAny(string str, char[] chars) @@ -145,13 +155,19 @@ private void RefreshRouteTable() { var routeKey = new RouteKey(AppAssembly, AdditionalAssemblies); - if (!routeKey.Equals(_currentRouteKey)) + if (!routeKey.Equals(_routeTableLastBuiltForRouteKey)) { - _currentRouteKey = routeKey; + _routeTableLastBuiltForRouteKey = routeKey; Routes = RouteTableFactory.Create(routeKey); } } + private void ClearRouteCaches() + { + RouteTableFactory.ClearCaches(); + _routeTableLastBuiltForRouteKey = default; + } + internal virtual void Refresh(bool isNavigationIntercepted) { // If an `OnNavigateAsync` task is currently in progress, then wait diff --git a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs index a88060552c8c..ff0632370c92 100644 --- a/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs +++ b/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs @@ -7,7 +7,11 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Reflection.Metadata; using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Components.HotReload; + +[assembly: MetadataUpdateHandler(typeof(Microsoft.AspNetCore.Components.Forms.EditContextDataAnnotationsExtensions))] namespace Microsoft.AspNetCore.Components.Forms { @@ -37,6 +41,13 @@ public static IDisposable EnableDataAnnotationsValidation(this EditContext editC return new DataAnnotationsEventSubscriptions(editContext); } + private static event Action? OnClearCache; + + private static void ClearCache(Type[]? _) + { + OnClearCache?.Invoke(); + } + private sealed class DataAnnotationsEventSubscriptions : IDisposable { private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new(); @@ -51,6 +62,11 @@ public DataAnnotationsEventSubscriptions(EditContext editContext) _editContext.OnFieldChanged += OnFieldChanged; _editContext.OnValidationRequested += OnValidationRequested; + + if (HotReloadFeature.IsSupported) + { + OnClearCache += ClearCache; + } } private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs) @@ -115,6 +131,11 @@ public void Dispose() _editContext.OnFieldChanged -= OnFieldChanged; _editContext.OnValidationRequested -= OnValidationRequested; _editContext.NotifyValidationStateChanged(); + + if (HotReloadFeature.IsSupported) + { + OnClearCache -= ClearCache; + } } private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo) @@ -132,6 +153,11 @@ private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier return propertyInfo != null; } + + internal void ClearCache() + { + _propertyInfoCache.Clear(); + } } } } diff --git a/src/Components/Forms/src/Microsoft.AspNetCore.Components.Forms.csproj b/src/Components/Forms/src/Microsoft.AspNetCore.Components.Forms.csproj index 7ac8a9203e0f..12b071a02268 100644 --- a/src/Components/Forms/src/Microsoft.AspNetCore.Components.Forms.csproj +++ b/src/Components/Forms/src/Microsoft.AspNetCore.Components.Forms.csproj @@ -9,8 +9,19 @@ true + + + + + + + + + + ILLink.Substitutions.xml + diff --git a/src/Components/Forms/src/Properties/ILLink.Substitutions.xml b/src/Components/Forms/src/Properties/ILLink.Substitutions.xml new file mode 100644 index 000000000000..6e9602f33f95 --- /dev/null +++ b/src/Components/Forms/src/Properties/ILLink.Substitutions.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index ad96e5e9c97f..0bf276fb2956 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -6,11 +6,14 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Reflection.Metadata; using System.Runtime.ExceptionServices; using System.Text; using System.Text.Json; using System.Threading.Tasks; +[assembly: MetadataUpdateHandler(typeof(Microsoft.JSInterop.Infrastructure.DotNetDispatcher))] + namespace Microsoft.JSInterop.Infrastructure { /// @@ -426,6 +429,12 @@ private static Assembly GetRequiredLoadedAssembly(AssemblyKey assemblyKey) ?? throw new ArgumentException($"There is no loaded assembly with the name '{assemblyKey.AssemblyName}'."); } + private static void ClearCache(Type[]? _) + { + _cachedMethodsByAssembly.Clear(); + _cachedMethodsByType.Clear(); + } + private readonly struct AssemblyKey : IEquatable { public AssemblyKey(Assembly assembly)