[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:
parent
b7c3b48353
commit
763ccb21b5
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue