From d937177e6b3dad81dc9e46af10157a1d15364c1a Mon Sep 17 00:00:00 2001 From: Steve Sanderson <SteveSandersonMS@users.noreply.github.com> Date: Tue, 10 Aug 2021 12:55:03 +0100 Subject: [PATCH] Dispatch dispose to sync context. Fixes #32411 (#35217) --- .../Components/src/RenderTree/Renderer.cs | 25 ++++++-- .../Components/test/RendererTest.cs | 62 +++++++++++++++++-- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 66aa9e4908f..ed3bc5951b6 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -43,6 +43,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree private ulong _lastEventHandlerId; private List<Task>? _pendingTasks; private Task? _disposeTask; + private bool _rendererIsDisposed; /// <summary> /// Allows the caller to handle exceptions from the SynchronizationContext when one is available. @@ -128,7 +129,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree /// <summary> /// Gets whether the renderer has been disposed. /// </summary> - internal bool Disposed { get; private set; } + internal bool Disposed => _rendererIsDisposed; private async void RenderRootComponentsOnHotReload() { @@ -582,9 +583,10 @@ namespace Microsoft.AspNetCore.Components.RenderTree /// </summary> protected virtual void ProcessPendingRender() { - if (Disposed) + if (_rendererIsDisposed) { - throw new ObjectDisposedException(nameof(Renderer), "Cannot process pending renders after the renderer has been disposed."); + // Once we're disposed, we'll disregard further attempts to render anything + return; } ProcessRenderQueue(); @@ -974,7 +976,17 @@ namespace Microsoft.AspNetCore.Components.RenderTree /// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param> protected virtual void Dispose(bool disposing) { - Disposed = true; + if (!Dispatcher.CheckAccess()) + { + // It's important that we only call the components' Dispose/DisposeAsync lifecycle methods + // on the sync context, like other lifecycle methods. In almost all cases we'd already be + // on the sync context here since DisposeAsync dispatches, but just in case someone is using + // Dispose directly, we'll dispatch and block. + Dispatcher.InvokeAsync(() => Dispose(disposing)).Wait(); + return; + } + + _rendererIsDisposed = true; if (TestableMetadataUpdate.IsSupported) { @@ -1079,7 +1091,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree /// <inheritdoc /> public async ValueTask DisposeAsync() { - if (Disposed) + if (_rendererIsDisposed) { return; } @@ -1090,7 +1102,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree } else { - Dispose(); + await Dispatcher.InvokeAsync(Dispose); + if (_disposeTask != null) { await _disposeTask; diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 152c3a841d9..a2b5288acc2 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -3930,15 +3930,24 @@ namespace Microsoft.AspNetCore.Components.Test } [Fact] - public void DisposingRenderer_RejectsAttemptsToStartMoreRenderBatches() + public void DisposingRenderer_DisregardsAttemptsToStartMoreRenderBatches() { // Arrange var renderer = new TestRenderer(); + var component = new TestComponent(builder => + { + builder.OpenElement(0, "my element"); + builder.AddContent(1, "some text"); + builder.CloseElement(); + }); + + // Act + renderer.AssignRootComponentId(component); renderer.Dispose(); + component.TriggerRender(); - // Act/Assert - var ex = Assert.Throws<ObjectDisposedException>(() => renderer.ProcessPendingRender()); - Assert.Contains("Cannot process pending renders after the renderer has been disposed.", ex.Message); + // Assert + Assert.Empty(renderer.Batches); } [Fact] @@ -4679,6 +4688,51 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Same(exception2, renderer.HandledExceptions[1]); } + [Fact] + public void DisposeCallsComponentDisposeOnSyncContext() + { + // Arrange + var renderer = new TestRenderer(); + var wasOnSyncContext = false; + var component = new DisposableComponent + { + DisposeAction = () => + { + wasOnSyncContext = renderer.Dispatcher.CheckAccess(); + } + }; + + // Act + var componentId = renderer.AssignRootComponentId(component); + renderer.Dispose(); + + // Assert + Assert.True(wasOnSyncContext); + } + + [Fact] + public async Task DisposeAsyncCallsComponentDisposeAsyncOnSyncContext() + { + // Arrange + var renderer = new TestRenderer(); + var wasOnSyncContext = false; + var component = new AsyncDisposableComponent + { + AsyncDisposeAction = () => + { + wasOnSyncContext = renderer.Dispatcher.CheckAccess(); + return ValueTask.CompletedTask; + } + }; + + // Act + var componentId = renderer.AssignRootComponentId(component); + await renderer.DisposeAsync(); + + // Assert + Assert.True(wasOnSyncContext); + } + private class TestComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new() { public List<Type> RequestedComponentTypes { get; } = new List<Type>(); -- GitLab