[Blazor] Missing IAsyncDisposable implementation on renderer (#26848)
* Implement async disposable on the renderer * Minor cleanups
This commit is contained in:
parent
905777ca5b
commit
901bf9ba43
|
|
@ -211,6 +211,7 @@ Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrameType.Text = 2 -> Micro
|
||||||
Microsoft.AspNetCore.Components.RenderTree.Renderer
|
Microsoft.AspNetCore.Components.RenderTree.Renderer
|
||||||
Microsoft.AspNetCore.Components.RenderTree.Renderer.AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent! component) -> int
|
Microsoft.AspNetCore.Components.RenderTree.Renderer.AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent! component) -> int
|
||||||
Microsoft.AspNetCore.Components.RenderTree.Renderer.Dispose() -> void
|
Microsoft.AspNetCore.Components.RenderTree.Renderer.Dispose() -> void
|
||||||
|
Microsoft.AspNetCore.Components.RenderTree.Renderer.DisposeAsync() -> System.Threading.Tasks.ValueTask
|
||||||
Microsoft.AspNetCore.Components.RenderTree.Renderer.ElementReferenceContext.get -> Microsoft.AspNetCore.Components.ElementReferenceContext?
|
Microsoft.AspNetCore.Components.RenderTree.Renderer.ElementReferenceContext.get -> Microsoft.AspNetCore.Components.ElementReferenceContext?
|
||||||
Microsoft.AspNetCore.Components.RenderTree.Renderer.ElementReferenceContext.set -> void
|
Microsoft.AspNetCore.Components.RenderTree.Renderer.ElementReferenceContext.set -> void
|
||||||
Microsoft.AspNetCore.Components.RenderTree.Renderer.GetCurrentRenderTreeFrames(int componentId) -> Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame>
|
Microsoft.AspNetCore.Components.RenderTree.Renderer.GetCurrentRenderTreeFrames(int componentId) -> Microsoft.AspNetCore.Components.RenderTree.ArrayRange<Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
||||||
//
|
//
|
||||||
// Provides mechanisms for rendering hierarchies of <see cref="IComponent"/> instances,
|
// Provides mechanisms for rendering hierarchies of <see cref="IComponent"/> instances,
|
||||||
// dispatching events to them, and notifying when the user interface is being updated.
|
// dispatching events to them, and notifying when the user interface is being updated.
|
||||||
public abstract partial class Renderer : IDisposable
|
public abstract partial class Renderer : IDisposable, IAsyncDisposable
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
|
private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
|
||||||
|
|
@ -36,6 +36,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
||||||
private ulong _lastEventHandlerId;
|
private ulong _lastEventHandlerId;
|
||||||
private List<Task> _pendingTasks;
|
private List<Task> _pendingTasks;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
private Task _disposeTask;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
|
/// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
|
||||||
|
|
@ -763,11 +764,34 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
||||||
// It's important that we handle all exceptions here before reporting any of them.
|
// It's important that we handle all exceptions here before reporting any of them.
|
||||||
// This way we can dispose all components before an error handler kicks in.
|
// This way we can dispose all components before an error handler kicks in.
|
||||||
List<Exception> exceptions = null;
|
List<Exception> exceptions = null;
|
||||||
|
List<Task> asyncDisposables = null;
|
||||||
foreach (var componentState in _componentStateById.Values)
|
foreach (var componentState in _componentStateById.Values)
|
||||||
{
|
{
|
||||||
Log.DisposingComponent(_logger, componentState);
|
Log.DisposingComponent(_logger, componentState);
|
||||||
|
|
||||||
if (componentState.Component is IDisposable disposable)
|
// Components shouldn't need to implement IAsyncDisposable and IDisposable simultaneously,
|
||||||
|
// but in case they do, we prefer the async overload since we understand the sync overload
|
||||||
|
// is implemented for more "constrained" scenarios.
|
||||||
|
// Component authors are responsible for their IAsyncDisposable implementations not taking
|
||||||
|
// forever.
|
||||||
|
if (componentState.Component is IAsyncDisposable asyncDisposable)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var task = asyncDisposable.DisposeAsync();
|
||||||
|
if (!task.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
|
asyncDisposables ??= new();
|
||||||
|
asyncDisposables.Add(task.AsTask());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
exceptions ??= new List<Exception>();
|
||||||
|
exceptions.Add(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (componentState.Component is IDisposable disposable)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -784,13 +808,42 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
||||||
_componentStateById.Clear(); // So we know they were all disposed
|
_componentStateById.Clear(); // So we know they were all disposed
|
||||||
_batchBuilder.Dispose();
|
_batchBuilder.Dispose();
|
||||||
|
|
||||||
if (exceptions?.Count > 1)
|
NotifyExceptions(exceptions);
|
||||||
|
|
||||||
|
if (asyncDisposables?.Count >= 1)
|
||||||
{
|
{
|
||||||
HandleException(new AggregateException("Exceptions were encountered while disposing components.", exceptions));
|
_disposeTask = HandleAsyncExceptions(asyncDisposables);
|
||||||
}
|
}
|
||||||
else if (exceptions?.Count == 1)
|
|
||||||
|
async Task HandleAsyncExceptions(List<Task> tasks)
|
||||||
{
|
{
|
||||||
HandleException(exceptions[0]);
|
List<Exception> asyncExceptions = null;
|
||||||
|
foreach (var task in tasks)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await task;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
asyncExceptions ??= new List<Exception>();
|
||||||
|
asyncExceptions.Add(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifyExceptions(asyncExceptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotifyExceptions(List<Exception> exceptions)
|
||||||
|
{
|
||||||
|
if (exceptions?.Count > 1)
|
||||||
|
{
|
||||||
|
HandleException(new AggregateException("Exceptions were encountered while disposing components.", exceptions));
|
||||||
|
}
|
||||||
|
else if (exceptions?.Count == 1)
|
||||||
|
{
|
||||||
|
HandleException(exceptions[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -801,5 +854,31 @@ namespace Microsoft.AspNetCore.Components.RenderTree
|
||||||
{
|
{
|
||||||
Dispose(disposing: true);
|
Dispose(disposing: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_disposeTask != null)
|
||||||
|
{
|
||||||
|
await _disposeTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
if (_disposeTask != null)
|
||||||
|
{
|
||||||
|
await _disposeTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await default(ValueTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3813,6 +3813,66 @@ namespace Microsoft.AspNetCore.Components.Test
|
||||||
Assert.Contains(exception2, aex.InnerExceptions);
|
Assert.Contains(exception2, aex.InnerExceptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DisposingRenderer_CapturesSyncExceptionsFromAllRegisteredAsyncDisposableComponents()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var renderer = new TestRenderer { ShouldHandleExceptions = true };
|
||||||
|
var exception1 = new InvalidOperationException();
|
||||||
|
var disposed = false;
|
||||||
|
|
||||||
|
var component = new TestComponent(builder =>
|
||||||
|
{
|
||||||
|
builder.AddContent(0, "Hello");
|
||||||
|
builder.OpenComponent<AsyncDisposableComponent>(1);
|
||||||
|
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => { disposed = true; throw exception1; }));
|
||||||
|
builder.CloseComponent();
|
||||||
|
});
|
||||||
|
var componentId = renderer.AssignRootComponentId(component);
|
||||||
|
component.TriggerRender();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderer.DisposeAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(disposed);
|
||||||
|
var handledException = Assert.Single(renderer.HandledExceptions);
|
||||||
|
Assert.Same(exception1, handledException);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DisposingRenderer_CapturesAsyncExceptionsFromAllRegisteredAsyncDisposableComponents()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var renderer = new TestRenderer { ShouldHandleExceptions = true };
|
||||||
|
var exception1 = new InvalidOperationException();
|
||||||
|
var disposed = false;
|
||||||
|
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
var component = new TestComponent(builder =>
|
||||||
|
{
|
||||||
|
builder.AddContent(0, "Hello");
|
||||||
|
builder.OpenComponent<AsyncDisposableComponent>(1);
|
||||||
|
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(async () => { await tcs.Task; disposed = true; throw exception1; }));
|
||||||
|
builder.CloseComponent();
|
||||||
|
});
|
||||||
|
var componentId = renderer.AssignRootComponentId(component);
|
||||||
|
component.TriggerRender();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var disposal = renderer.DisposeAsync();
|
||||||
|
Assert.False(disposed);
|
||||||
|
Assert.False(disposal.IsCompleted);
|
||||||
|
|
||||||
|
tcs.TrySetResult();
|
||||||
|
await disposal;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(disposed);
|
||||||
|
var handledException = Assert.Single(renderer.HandledExceptions);
|
||||||
|
Assert.Same(exception1, handledException);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(null)] // No existing attribute to update
|
[InlineData(null)] // No existing attribute to update
|
||||||
[InlineData("old property value")] // Has existing attribute to update
|
[InlineData("old property value")] // Has existing attribute to update
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Renderer.Dispose();
|
await Renderer.DisposeAsync();
|
||||||
|
|
||||||
// This cast is needed because it's possible the scope may not support async dispose.
|
// This cast is needed because it's possible the scope may not support async dispose.
|
||||||
// Our DI container does, but other DI systems may not.
|
// Our DI container does, but other DI systems may not.
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|
||||||
_renderer?.Dispose();
|
if (_renderer != null)
|
||||||
|
{
|
||||||
|
await _renderer.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (_scope is IAsyncDisposable asyncDisposableScope)
|
if (_scope is IAsyncDisposable asyncDisposableScope)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue