Skip to content

Commit 7e3dcf7

Browse files
Ensure ComponentState is always disposed (#48406)
1 parent 4e17e96 commit 7e3dcf7

File tree

5 files changed

+86
-112
lines changed

5 files changed

+86
-112
lines changed

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ Microsoft.AspNetCore.Components.Rendering.ComponentState
3434
Microsoft.AspNetCore.Components.Rendering.ComponentState.Component.get -> Microsoft.AspNetCore.Components.IComponent!
3535
Microsoft.AspNetCore.Components.Rendering.ComponentState.ComponentId.get -> int
3636
Microsoft.AspNetCore.Components.Rendering.ComponentState.ComponentState(Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer, int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> void
37-
Microsoft.AspNetCore.Components.Rendering.ComponentState.Dispose() -> void
3837
Microsoft.AspNetCore.Components.Rendering.ComponentState.ParentComponentState.get -> Microsoft.AspNetCore.Components.Rendering.ComponentState?
3938
Microsoft.AspNetCore.Components.RenderTree.Renderer.GetComponentState(int componentId) -> Microsoft.AspNetCore.Components.Rendering.ComponentState!
4039
Microsoft.AspNetCore.Components.Sections.SectionContent
@@ -61,6 +60,7 @@ override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int
6160
override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool
6261
override Microsoft.AspNetCore.Components.EventCallback<TValue>.GetHashCode() -> int
6362
override Microsoft.AspNetCore.Components.EventCallback<TValue>.Equals(object? obj) -> bool
63+
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask
6464
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
6565
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState!
6666
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task!

src/Components/Components/src/RenderTree/Renderer.cs

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -876,28 +876,20 @@ private void ProcessDisposalQueueInExistingBatch()
876876
var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue();
877877
var disposeComponentState = GetRequiredComponentState(disposeComponentId);
878878
Log.DisposingComponent(_logger, disposeComponentState);
879-
if (!(disposeComponentState.Component is IAsyncDisposable))
880-
{
881-
if (!disposeComponentState.TryDisposeInBatch(_batchBuilder, out var exception))
882-
{
883-
exceptions ??= new List<Exception>();
884-
exceptions.Add(exception);
885-
}
886-
}
887-
else
879+
880+
try
888881
{
889-
var result = disposeComponentState.DisposeInBatchAsync(_batchBuilder);
890-
if (result.IsCompleted)
882+
var disposalTask = disposeComponentState.DisposeInBatchAsync(_batchBuilder);
883+
if (disposalTask.IsCompletedSuccessfully)
891884
{
892-
if (!result.IsCompletedSuccessfully)
893-
{
894-
exceptions ??= new List<Exception>();
895-
exceptions.Add(result.Exception);
896-
}
885+
// If it's a IValueTaskSource backed ValueTask,
886+
// inform it its result has been read so it can reset
887+
disposalTask.GetAwaiter().GetResult();
897888
}
898889
else
899890
{
900891
// We set owningComponentState to null because we don't want exceptions during disposal to be recoverable
892+
var result = disposalTask.AsTask();
901893
AddToPendingTasksWithErrorHandling(GetHandledAsynchronousDisposalErrorsTask(result), owningComponentState: null);
902894

903895
async Task GetHandledAsynchronousDisposalErrorsTask(Task result)
@@ -913,6 +905,11 @@ async Task GetHandledAsynchronousDisposalErrorsTask(Task result)
913905
}
914906
}
915907
}
908+
catch (Exception exception)
909+
{
910+
exceptions ??= new List<Exception>();
911+
exceptions.Add(exception);
912+
}
916913

917914
_componentStateById.Remove(disposeComponentId);
918915
_componentStateByComponent.Remove(disposeComponentState.Component);
@@ -1080,39 +1077,25 @@ protected virtual void Dispose(bool disposing)
10801077
{
10811078
Log.DisposingComponent(_logger, componentState);
10821079

1083-
// Components shouldn't need to implement IAsyncDisposable and IDisposable simultaneously,
1084-
// but in case they do, we prefer the async overload since we understand the sync overload
1085-
// is implemented for more "constrained" scenarios.
1086-
// Component authors are responsible for their IAsyncDisposable implementations not taking
1087-
// forever.
1088-
if (componentState.Component is IAsyncDisposable asyncDisposable)
1080+
try
10891081
{
1090-
try
1082+
var task = componentState.DisposeAsync();
1083+
if (task.IsCompletedSuccessfully)
10911084
{
1092-
var task = asyncDisposable.DisposeAsync();
1093-
if (!task.IsCompletedSuccessfully)
1094-
{
1095-
asyncDisposables ??= new();
1096-
asyncDisposables.Add(task.AsTask());
1097-
}
1085+
// If it's a IValueTaskSource backed ValueTask,
1086+
// inform it its result has been read so it can reset
1087+
task.GetAwaiter().GetResult();
10981088
}
1099-
catch (Exception exception)
1089+
else
11001090
{
1101-
exceptions ??= new List<Exception>();
1102-
exceptions.Add(exception);
1091+
asyncDisposables ??= new();
1092+
asyncDisposables.Add(task.AsTask());
11031093
}
11041094
}
1105-
else if (componentState.Component is IDisposable disposable)
1095+
catch (Exception exception)
11061096
{
1107-
try
1108-
{
1109-
componentState.Dispose();
1110-
}
1111-
catch (Exception exception)
1112-
{
1113-
exceptions ??= new List<Exception>();
1114-
exceptions.Add(exception);
1115-
}
1097+
exceptions ??= new List<Exception>();
1098+
exceptions.Add(exception);
11161099
}
11171100
}
11181101

src/Components/Components/src/Rendering/ComponentState.cs

Lines changed: 23 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5-
using System.Diagnostics.CodeAnalysis;
65
using Microsoft.AspNetCore.Components.RenderTree;
76

87
namespace Microsoft.AspNetCore.Components.Rendering;
@@ -13,7 +12,7 @@ namespace Microsoft.AspNetCore.Components.Rendering;
1312
/// detail of <see cref="Renderer"/>.
1413
/// </summary>
1514
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
16-
public class ComponentState : IDisposable
15+
public class ComponentState : IAsyncDisposable
1716
{
1817
private readonly Renderer _renderer;
1918
private readonly IReadOnlyList<CascadingParameterState> _cascadingParameters;
@@ -108,41 +107,6 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re
108107
batchBuilder.InvalidateParameterViews();
109108
}
110109

111-
internal bool TryDisposeInBatch(RenderBatchBuilder batchBuilder, [NotNullWhen(false)] out Exception? exception)
112-
{
113-
_componentWasDisposed = true;
114-
exception = null;
115-
116-
try
117-
{
118-
if (Component is IDisposable disposable)
119-
{
120-
disposable.Dispose();
121-
}
122-
}
123-
catch (Exception ex)
124-
{
125-
exception = ex;
126-
}
127-
128-
CleanupComponentStateResources(batchBuilder);
129-
130-
return exception == null;
131-
}
132-
133-
private void CleanupComponentStateResources(RenderBatchBuilder batchBuilder)
134-
{
135-
// We don't expect these things to throw.
136-
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrentRenderTree.GetFrames());
137-
138-
if (_hasAnyCascadingParameterSubscriptions)
139-
{
140-
RemoveCascadingParameterSubscriptions();
141-
}
142-
143-
DisposeBuffers();
144-
}
145-
146110
// Callers expect this method to always return a faulted task.
147111
internal Task NotifyRenderCompletedAsync()
148112
{
@@ -254,15 +218,26 @@ private void RemoveCascadingParameterSubscriptions()
254218
}
255219

256220
/// <summary>
257-
/// Disposes this instance.
221+
/// Disposes this instance and its associated component.
258222
/// </summary>
259-
public void Dispose()
223+
public virtual ValueTask DisposeAsync()
260224
{
225+
_componentWasDisposed = true;
261226
DisposeBuffers();
262227

263-
if (Component is IDisposable disposable)
228+
// Components shouldn't need to implement IAsyncDisposable and IDisposable simultaneously,
229+
// but in case they do, we prefer the async overload since we understand the sync overload
230+
// is implemented for more "constrained" scenarios.
231+
// Component authors are responsible for their IAsyncDisposable implementations not taking
232+
// forever.
233+
if (Component is IAsyncDisposable asyncDisposable)
264234
{
265-
disposable.Dispose();
235+
return asyncDisposable.DisposeAsync();
236+
}
237+
else
238+
{
239+
(Component as IDisposable)?.Dispose();
240+
return ValueTask.CompletedTask;
266241
}
267242
}
268243

@@ -273,33 +248,17 @@ private void DisposeBuffers()
273248
_latestDirectParametersSnapshot?.Dispose();
274249
}
275250

276-
internal Task DisposeInBatchAsync(RenderBatchBuilder batchBuilder)
251+
internal ValueTask DisposeInBatchAsync(RenderBatchBuilder batchBuilder)
277252
{
278-
_componentWasDisposed = true;
279-
280-
CleanupComponentStateResources(batchBuilder);
253+
// We don't expect these things to throw.
254+
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrentRenderTree.GetFrames());
281255

282-
try
283-
{
284-
var result = ((IAsyncDisposable)Component).DisposeAsync();
285-
if (result.IsCompletedSuccessfully)
286-
{
287-
// If it's a IValueTaskSource backed ValueTask,
288-
// inform it its result has been read so it can reset
289-
result.GetAwaiter().GetResult();
290-
return Task.CompletedTask;
291-
}
292-
else
293-
{
294-
// We know we are dealing with an exception that happened asynchronously, so return a task
295-
// to the caller so that he can unwrap it.
296-
return result.AsTask();
297-
}
298-
}
299-
catch (Exception e)
256+
if (_hasAnyCascadingParameterSubscriptions)
300257
{
301-
return Task.FromException(e);
258+
RemoveCascadingParameterSubscriptions();
302259
}
260+
261+
return DisposeAsync();
303262
}
304263

305264
private string GetDebuggerDisplay()

src/Components/Components/test/RendererTest.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2334,9 +2334,8 @@ public void RenderBatch_HandlesSynchronousExceptionsInAsyncDisposableComponents(
23342334

23352335
// Outer component is still alive and not disposed.
23362336
Assert.False(component.Disposed);
2337-
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
2338-
var innerException = Assert.Single(aex.Flatten().InnerExceptions);
2339-
Assert.Same(exception1, innerException);
2337+
var aex = Assert.Single(renderer.HandledExceptions);
2338+
Assert.Same(exception1, aex);
23402339
}
23412340

23422341
[Fact]
@@ -2493,8 +2492,7 @@ public void RenderBatch_ReportsSynchronousCancelationsAsErrors()
24932492

24942493
// Outer component is still alive and not disposed.
24952494
Assert.False(component.Disposed);
2496-
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
2497-
Assert.IsType<TaskCanceledException>(Assert.Single(aex.Flatten().InnerExceptions));
2495+
Assert.IsType<TaskCanceledException>(Assert.Single(renderer.HandledExceptions));
24982496
}
24992497

25002498
[Fact]

src/Components/Shared/test/TestRenderer.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Runtime.ExceptionServices;
5+
using Microsoft.AspNetCore.Components.Rendering;
56
using Microsoft.AspNetCore.Components.RenderTree;
67
using Microsoft.Extensions.Logging.Abstractions;
78

@@ -60,6 +61,8 @@ protected internal override bool ShouldTrackNamedEventHandlers()
6061

6162
public Task NextRenderResultTask { get; set; } = Task.CompletedTask;
6263

64+
private HashSet<TestRendererComponentState> UndisposedComponentStates { get; } = new();
65+
6366
public new int AssignRootComponentId(IComponent component)
6467
=> base.AssignRootComponentId(component);
6568

@@ -145,4 +148,35 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
145148

146149
public new void ProcessPendingRender()
147150
=> base.ProcessPendingRender();
151+
152+
protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState parentComponentState)
153+
=> new TestRendererComponentState(this, componentId, component, parentComponentState);
154+
155+
protected override void Dispose(bool disposing)
156+
{
157+
base.Dispose(disposing);
158+
159+
if (UndisposedComponentStates.Count > 0)
160+
{
161+
throw new InvalidOperationException("Did not dispose all the ComponentState instances. This could lead to ArrayBuffer not returning buffers to its pool.");
162+
}
163+
}
164+
165+
class TestRendererComponentState : ComponentState, IAsyncDisposable
166+
{
167+
private readonly TestRenderer _renderer;
168+
169+
public TestRendererComponentState(Renderer renderer, int componentId, IComponent component, ComponentState parentComponentState)
170+
: base(renderer, componentId, component, parentComponentState)
171+
{
172+
_renderer = (TestRenderer)renderer;
173+
_renderer.UndisposedComponentStates.Add(this);
174+
}
175+
176+
public override ValueTask DisposeAsync()
177+
{
178+
_renderer.UndisposedComponentStates.Remove(this);
179+
return base.DisposeAsync();
180+
}
181+
}
148182
}

0 commit comments

Comments
 (0)