[Blazor] Async disposable support for Blazor (#23813)

* [Blazor] Support IAsyncDisposable in components
* Handles async disposal of components within the Blazor pipeline.
* Renders remain synchronous and don't wait for disposal to complete.
* Synchronous disposal executions remain inlined.
* Async disposal executions can trigger renders in different batches.
* Async disposals are handled individually and not grouped based on the batch they were generated on.
This commit is contained in:
Javier Calvarro Nelson 2020-07-16 13:21:56 +02:00 committed by GitHub
parent b7c3b48353
commit 763ccb21b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 461 additions and 8 deletions

View File

@ -1427,6 +1427,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.Web.Extensions.Tests", "src\Components\Web.Extensions\test\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj", "{157605CB-5170-4C1A-980F-4BAE42DB60DE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{6126DCE4-9692-4EE2-B240-C65743572995}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicTestApp", "src\Components\test\testassets\BasicTestApp\BasicTestApp.csproj", "{46FB7E93-1294-4068-B80A-D4864F78277A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComponentsApp.App", "src\Components\test\testassets\ComponentsApp.App\ComponentsApp.App.csproj", "{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComponentsApp.Server", "src\Components\test\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj", "{19974360-4A63-425A-94DB-C2C940A3A97A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyTestContentPackage", "src\Components\test\testassets\LazyTestContentPackage\LazyTestContentPackage.csproj", "{ADF9C126-F322-4E34-AFD3-E626A4487206}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestContentPackage", "src\Components\test\testassets\TestContentPackage\TestContentPackage.csproj", "{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components.TestServer", "src\Components\test\testassets\TestServer\Components.TestServer.csproj", "{8A59AF88-4A82-46ED-977D-D909001F8107}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -6729,6 +6743,78 @@ Global
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x64.Build.0 = Release|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.ActiveCfg = Release|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.Build.0 = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x64.ActiveCfg = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x64.Build.0 = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x86.ActiveCfg = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x86.Build.0 = Debug|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|Any CPU.Build.0 = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|x64.ActiveCfg = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|x64.Build.0 = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|x86.ActiveCfg = Release|Any CPU
{46FB7E93-1294-4068-B80A-D4864F78277A}.Release|x86.Build.0 = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|x64.ActiveCfg = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|x64.Build.0 = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|x86.ActiveCfg = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Debug|x86.Build.0 = Debug|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|Any CPU.Build.0 = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|x64.ActiveCfg = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|x64.Build.0 = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|x86.ActiveCfg = Release|Any CPU
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16}.Release|x86.Build.0 = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|x64.ActiveCfg = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|x64.Build.0 = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|x86.ActiveCfg = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Debug|x86.Build.0 = Debug|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|Any CPU.Build.0 = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|x64.ActiveCfg = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|x64.Build.0 = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|x86.ActiveCfg = Release|Any CPU
{19974360-4A63-425A-94DB-C2C940A3A97A}.Release|x86.Build.0 = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|x64.ActiveCfg = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|x64.Build.0 = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|x86.ActiveCfg = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Debug|x86.Build.0 = Debug|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|Any CPU.Build.0 = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|x64.ActiveCfg = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|x64.Build.0 = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|x86.ActiveCfg = Release|Any CPU
{ADF9C126-F322-4E34-AFD3-E626A4487206}.Release|x86.Build.0 = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|x64.Build.0 = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|x86.ActiveCfg = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Debug|x86.Build.0 = Debug|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|Any CPU.Build.0 = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|x64.ActiveCfg = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|x64.Build.0 = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|x86.ActiveCfg = Release|Any CPU
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}.Release|x86.Build.0 = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|x64.Build.0 = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Debug|x86.Build.0 = Debug|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|Any CPU.Build.0 = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x64.ActiveCfg = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x64.Build.0 = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x86.ActiveCfg = Release|Any CPU
{8A59AF88-4A82-46ED-977D-D909001F8107}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -7444,6 +7530,13 @@ Global
{F71FE795-9923-461B-9809-BB1821A276D0} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
{8294A74F-7DAA-4B69-BC56-7634D93C9693} = {F71FE795-9923-461B-9809-BB1821A276D0}
{157605CB-5170-4C1A-980F-4BAE42DB60DE} = {F71FE795-9923-461B-9809-BB1821A276D0}
{6126DCE4-9692-4EE2-B240-C65743572995} = {0508E463-0269-40C9-B5C2-3B600FB2A28B}
{46FB7E93-1294-4068-B80A-D4864F78277A} = {6126DCE4-9692-4EE2-B240-C65743572995}
{25FA84DB-EEA7-4068-8E2D-F3D48B281C16} = {6126DCE4-9692-4EE2-B240-C65743572995}
{19974360-4A63-425A-94DB-C2C940A3A97A} = {6126DCE4-9692-4EE2-B240-C65743572995}
{ADF9C126-F322-4E34-AFD3-E626A4487206} = {6126DCE4-9692-4EE2-B240-C65743572995}
{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31} = {6126DCE4-9692-4EE2-B240-C65743572995}
{8A59AF88-4A82-46ED-977D-D909001F8107} = {6126DCE4-9692-4EE2-B240-C65743572995}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

View File

@ -498,6 +498,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
{
ProcessRenderQueue();
}
ComponentsProfiling.Instance.End();
}
@ -634,11 +635,43 @@ namespace Microsoft.AspNetCore.Components.RenderTree
var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue();
var disposeComponentState = GetRequiredComponentState(disposeComponentId);
Log.DisposingComponent(_logger, disposeComponentState);
if (!disposeComponentState.TryDisposeInBatch(_batchBuilder, out var exception))
if (!(disposeComponentState.Component is IAsyncDisposable))
{
exceptions ??= new List<Exception>();
exceptions.Add(exception);
if (!disposeComponentState.TryDisposeInBatch(_batchBuilder, out var exception))
{
exceptions ??= new List<Exception>();
exceptions.Add(exception);
}
}
else
{
var result = disposeComponentState.DisposeInBatchAsync(_batchBuilder);
if (result.IsCompleted)
{
if (!result.IsCompletedSuccessfully)
{
exceptions ??= new List<Exception>();
exceptions.Add(result.Exception);
}
}
else
{
AddToPendingTasks(GetHandledAsynchronousDisposalErrorsTask(result));
async Task GetHandledAsynchronousDisposalErrorsTask(Task result)
{
try
{
await result;
}
catch (Exception e)
{
HandleException(e);
}
}
}
}
_componentStateById.Remove(disposeComponentId);
_batchBuilder.DisposedComponentIds.Append(disposeComponentId);
}

View File

@ -101,6 +101,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
exception = ex;
}
CleanupComponentStateResources(batchBuilder);
return exception == null;
}
private void CleanupComponentStateResources(RenderBatchBuilder batchBuilder)
{
// We don't expect these things to throw.
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrentRenderTree.GetFrames());
@ -110,8 +117,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
DisposeBuffers();
return exception == null;
}
// Callers expect this method to always return a faulted task.
@ -222,5 +227,31 @@ namespace Microsoft.AspNetCore.Components.Rendering
((IDisposable)CurrentRenderTree).Dispose();
_latestDirectParametersSnapshot?.Dispose();
}
public Task DisposeInBatchAsync(RenderBatchBuilder batchBuilder)
{
_componentWasDisposed = true;
CleanupComponentStateResources(batchBuilder);
try
{
var result = ((IAsyncDisposable)Component).DisposeAsync();
if (result.IsCompletedSuccessfully)
{
return Task.CompletedTask;
}
else
{
// We know we are dealing with an exception that happened asynchronously, so return a task
// to the caller so that he can unwrap it.
return result.AsTask();
}
}
catch (Exception e)
{
return Task.FromException(e);
}
}
}
}

View File

@ -487,7 +487,8 @@ namespace Microsoft.AspNetCore.Components.Test
public void DispatchEventHandlesSynchronousExceptionsFromEventHandlers()
{
// Arrange: Render a component with an event handler
var renderer = new TestRenderer {
var renderer = new TestRenderer
{
ShouldHandleExceptions = true
};
@ -2086,6 +2087,238 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Contains(exception2, aex.InnerExceptions);
}
[Fact]
public void RenderBatch_HandlesSynchronousExceptionsInAsyncDisposableComponents()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var exception1 = new InvalidOperationException();
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => throw exception1));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
var innerException = Assert.Single(aex.Flatten().InnerExceptions);
Assert.Same(exception1, innerException);
}
[Fact]
public void RenderBatch_CanDisposeSynchrounousAsyncDisposableImplementations()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => default));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void RenderBatch_CanDisposeAsynchronousAsyncDisposables()
{
// Arrange
var semaphore = new Semaphore(0, 1);
var renderer = new TestRenderer { ShouldHandleExceptions = true };
renderer.OnExceptionHandled = () => semaphore.Release();
var exception1 = new InvalidOperationException();
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(async () => { await tcs.Task; }));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
// Continue execution
tcs.SetResult();
Assert.False(semaphore.WaitOne(10));
Assert.Empty(renderer.HandledExceptions);
}
[Fact]
public void RenderBatch_HandlesAsynchronousExceptionsInAsyncDisposableComponents()
{
// Arrange
var semaphore = new Semaphore(0, 1);
var renderer = new TestRenderer { ShouldHandleExceptions = true };
renderer.OnExceptionHandled = () => semaphore.Release();
var exception1 = new InvalidOperationException();
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(async () => { await tcs.Task; throw exception1; }));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
// Continue execution
tcs.SetResult();
semaphore.WaitOne();
var aex = Assert.IsType<InvalidOperationException>(Assert.Single(renderer.HandledExceptions));
Assert.Same(exception1, aex);
}
[Fact]
public void RenderBatch_ReportsSynchronousCancelationsAsErrors()
{
// Arrange
var renderer = new TestRenderer { ShouldHandleExceptions = true };
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(1, nameof(AsyncDisposableComponent.AsyncDisposeAction), (Func<ValueTask>)(() => throw new TaskCanceledException()));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
Assert.IsType<TaskCanceledException>(Assert.Single(aex.Flatten().InnerExceptions));
}
[Fact]
public void RenderBatch_ReportsAsynchronousCancelationsAsErrors()
{
// Arrange
var semaphore = new Semaphore(0, 1);
var renderer = new TestRenderer { ShouldHandleExceptions = true };
renderer.OnExceptionHandled += () => semaphore.Release();
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var firstRender = true;
var component = new TestComponent(builder =>
{
if (firstRender)
{
builder.AddContent(0, "Hello");
builder.OpenComponent<AsyncDisposableComponent>(1);
builder.AddAttribute(
1,
nameof(AsyncDisposableComponent.AsyncDisposeAction),
(Func<ValueTask>)(() => new ValueTask(tcs.Task)));
builder.CloseComponent();
}
});
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act: Second render
firstRender = false;
component.TriggerRender();
// Assert: Applicable children are included in disposal list
Assert.Equal(2, renderer.Batches.Count);
Assert.Equal(new[] { 1, }, renderer.Batches[1].DisposedComponentIDs);
// Outer component is still alive and not disposed.
Assert.False(component.Disposed);
Assert.Empty(renderer.HandledExceptions);
// Cancel execution
tcs.SetCanceled();
semaphore.WaitOne();
var aex = Assert.IsType<TaskCanceledException>(Assert.Single(renderer.HandledExceptions));
}
[Fact]
public void RenderBatch_DoesNotDisposeComponentMultipleTimes()
{
@ -2589,7 +2822,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act: Toggle the checkbox
var eventArgs = new ChangeEventArgs { Value = true };
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
var renderTask = renderer.DispatchEventAsync(checkboxChangeEventHandlerId, eventArgs);
Assert.True(renderTask.IsCompletedSuccessfully);
var latestBatch = renderer.Batches.Last();
@ -3768,7 +4001,7 @@ namespace Microsoft.AspNetCore.Components.Test
requestedType => Assert.Equal(typeof(TestComponent), requestedType));
}
private class TestComponentActivator<TResult> : IComponentActivator where TResult: IComponent, new()
private class TestComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
{
public List<Type> RequestedComponentTypes { get; } = new List<Type>();
@ -4147,6 +4380,24 @@ namespace Microsoft.AspNetCore.Components.Test
}
}
private class AsyncDisposableComponent : AutoRenderComponent, IAsyncDisposable
{
public bool Disposed { get; private set; }
[Parameter]
public Func<ValueTask> AsyncDisposeAction { get; set; }
public ValueTask DisposeAsync()
{
Disposed = true;
return AsyncDisposeAction == null ? default : AsyncDisposeAction.Invoke();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
}
}
class TestAsyncRenderer : TestRenderer
{
public Task NextUpdateDisplayReturnTask { get; set; }

View File

@ -44,6 +44,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Browser.Equal("1", () => Browser.FindElement(By.Id("count")).Text);
}
[Fact]
public void PrerenderingWaitsForAsyncDisposableComponents()
{
Navigate("/prerendered/prerendered-async-disposal");
// Prerendered output shows "not connected"
Browser.Equal("After async disposal", () => Browser.FindElement(By.Id("disposal-message")).Text);
}
[Fact]
public void CanUseJSInteropFromOnAfterRenderAsync()
{

View File

@ -0,0 +1,11 @@
@implements IAsyncDisposable
@code{
[Parameter] public EventCallback<string> SetMessage { get; set; }
public async ValueTask DisposeAsync()
{
await SetMessage.InvokeAsync("Before async disposal");
await Task.Yield();
await SetMessage.InvokeAsync("After async disposal");
}
}

View File

@ -0,0 +1,25 @@
@page "/prerendered-async-disposal"
<p>
This component shows that prerendering will work for components that implement IAsyncDisposable to finish
disposing before rendering the output as html.
</p>
<p id="disposal-message">@_message</p>
@if (!_hideComponent)
{
<AsyncDisposableComponent SetMessage="@SetMessage" />
}
@code {
private bool _hideComponent = false;
private string _message = "Uninitialized";
protected override async Task OnInitializedAsync()
{
await Task.Yield();
_hideComponent = true;
}
private void SetMessage(string message) => _message = message;
}