diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs index 58a7ac8d656f..06d3113f36e3 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs @@ -28,6 +28,9 @@ internal class RazorComponentEndpointDataSource<[DynamicallyAccessedMembers(Comp private List? _endpoints; private CancellationTokenSource _cancellationTokenSource; private IChangeToken _changeToken; + private IDisposable? _disposableChangeToken; // THREADING: protected by _lock + + public Func SetDisposableChangeTokenAction = disposableChangeToken => disposableChangeToken; // Internal for testing. internal ComponentApplicationBuilder Builder => _builder; @@ -45,6 +48,7 @@ public RazorComponentEndpointDataSource( _renderModeEndpointProviders = renderModeEndpointProviders.ToArray(); _factory = factory; _hotReloadService = hotReloadService; + HotReloadService.ClearCacheEvent += OnHotReloadClearCache; DefaultBuilder = new RazorComponentsEndpointConventionBuilder( _lock, builder, @@ -139,12 +143,23 @@ private void UpdateEndpoints() _cancellationTokenSource = new CancellationTokenSource(); _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token); oldCancellationTokenSource?.Cancel(); + oldCancellationTokenSource?.Dispose(); if (_hotReloadService is { MetadataUpdateSupported : true }) { - ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints); + _disposableChangeToken?.Dispose(); + _disposableChangeToken = SetDisposableChangeTokenAction(ChangeToken.OnChange(_hotReloadService.GetChangeToken, UpdateEndpoints)); } } } + + public void OnHotReloadClearCache(Type[]? types) + { + lock (_lock) + { + _disposableChangeToken?.Dispose(); + _disposableChangeToken = null; + } + } public override IChangeToken GetChangeToken() { diff --git a/src/Components/Endpoints/src/DependencyInjection/HotReloadService.cs b/src/Components/Endpoints/src/DependencyInjection/HotReloadService.cs index 51680ca61034..ad3c985b564f 100644 --- a/src/Components/Endpoints/src/DependencyInjection/HotReloadService.cs +++ b/src/Components/Endpoints/src/DependencyInjection/HotReloadService.cs @@ -18,6 +18,7 @@ public HotReloadService() private CancellationTokenSource _tokenSource = new(); private static event Action? UpdateApplicationEvent; + internal static event Action? ClearCacheEvent; public bool MetadataUpdateSupported { get; internal set; } @@ -27,11 +28,17 @@ public static void UpdateApplication(Type[]? changedTypes) { UpdateApplicationEvent?.Invoke(changedTypes); } + + public static void ClearCache(Type[]? types) + { + ClearCacheEvent?.Invoke(types); + } private void NotifyUpdateApplication(Type[]? changedTypes) { var current = Interlocked.Exchange(ref _tokenSource, new CancellationTokenSource()); current.Cancel(); + current.Dispose(); } public void Dispose() diff --git a/src/Components/Endpoints/test/HotReloadServiceTests.cs b/src/Components/Endpoints/test/HotReloadServiceTests.cs index a85697e9ec4d..a492c36e39b0 100644 --- a/src/Components/Endpoints/test/HotReloadServiceTests.cs +++ b/src/Components/Endpoints/test/HotReloadServiceTests.cs @@ -137,6 +137,52 @@ public void NotifiesCompositeEndpointDataSource() Assert.Empty(compositeEndpointDataSource.Endpoints); } + private sealed class WrappedChangeTokenDisposable : IDisposable + { + public bool IsDisposed { get; private set; } + private readonly IDisposable _innerDisposable; + + public WrappedChangeTokenDisposable(IDisposable innerDisposable) + { + _innerDisposable = innerDisposable; + } + + public void Dispose() + { + IsDisposed = true; + _innerDisposable.Dispose(); + } + } + + [Fact] + public void ConfirmChangeTokenDisposedHotReload() + { + // Arrange + var builder = CreateBuilder(typeof(ServerComponent)); + var services = CreateServices(typeof(MockEndpointProvider)); + var endpointDataSource = CreateDataSource(builder, services); + + WrappedChangeTokenDisposable wrappedChangeTokenDisposable = null; + + endpointDataSource.SetDisposableChangeTokenAction = (IDisposable disposableChangeToken) => { + wrappedChangeTokenDisposable = new WrappedChangeTokenDisposable(disposableChangeToken); + return wrappedChangeTokenDisposable; + }; + + var endpoint = Assert.IsType(Assert.Single(endpointDataSource.Endpoints)); + Assert.Equal("/server", endpoint.RoutePattern.RawText); + Assert.DoesNotContain(endpoint.Metadata, (element) => element is TestMetadata); + + // Make a modification and then perform a hot reload. + endpointDataSource.Conventions.Add(builder => + builder.Metadata.Add(new TestMetadata())); + HotReloadService.UpdateApplication(null); + HotReloadService.ClearCache(null); + + // Confirm the change token is disposed after ClearCache + Assert.True(wrappedChangeTokenDisposable.IsDisposed); + } + private class TestMetadata { } private ComponentApplicationBuilder CreateBuilder(params Type[] types)