Skip to content

Commit b56c589

Browse files
authored
Dispose components on client disconnects (#6693)
* Dispose components on client disconnects Fixes #4047
1 parent 4c956d4 commit b56c589

File tree

9 files changed

+246
-30
lines changed

9 files changed

+246
-30
lines changed

src/Components/.editorconfig

+28
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,31 @@ indent_size = 2
2525

2626
[*.{xml,csproj,config,*proj,targets,props}]
2727
indent_size = 2
28+
29+
# Dotnet code style settings:
30+
[*.cs]
31+
# Sort using and Import directives with System.* appearing first
32+
dotnet_sort_system_directives_first = true
33+
34+
# Don't use this. qualifier
35+
dotnet_style_qualification_for_field = false:suggestion
36+
dotnet_style_qualification_for_property = false:suggestion
37+
38+
# use int x = .. over Int32
39+
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
40+
41+
# use int.MaxValue over Int32.MaxValue
42+
dotnet_style_predefined_type_for_member_access = true:suggestion
43+
44+
# Require var all the time.
45+
csharp_style_var_for_built_in_types = true:suggestion
46+
csharp_style_var_when_type_is_apparent = true:suggestion
47+
csharp_style_var_elsewhere = true:suggestion
48+
49+
# Newline settings
50+
csharp_new_line_before_open_brace = all
51+
csharp_new_line_before_else = true
52+
csharp_new_line_before_catch = true
53+
csharp_new_line_before_finally = true
54+
csharp_new_line_before_members_in_object_initializers = true
55+
csharp_new_line_before_members_in_anonymous_types = true

src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Rendering/WebAssemblyRenderer.cs

+4-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
1515
/// Provides mechanisms for rendering <see cref="IComponent"/> instances in a
1616
/// web browser, dispatching events to them, and refreshing the UI as required.
1717
/// </summary>
18-
public class WebAssemblyRenderer : Renderer, IDisposable
18+
public class WebAssemblyRenderer : Renderer
1919
{
2020
private readonly int _webAssemblyRendererId;
2121

@@ -71,11 +71,10 @@ public void AddComponent(Type componentType, string domElementSelector)
7171
RenderRootComponent(componentId);
7272
}
7373

74-
/// <summary>
75-
/// Disposes the instance.
76-
/// </summary>
77-
public void Dispose()
74+
/// <inheritdoc />
75+
protected override void Dispose(bool disposing)
7876
{
77+
base.Dispose(disposing);
7978
RendererRegistry.Current.TryRemove(_webAssemblyRendererId);
8079
}
8180

src/Components/src/Microsoft.AspNetCore.Components.Browser/Properties/AssemblyInfo.cs

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22

33
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
44
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
5+
6+
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/CircuitHost.cs

+6-10
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,26 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
1616
{
1717
internal class CircuitHost : IDisposable
1818
{
19-
private static AsyncLocal<CircuitHost> _current = new AsyncLocal<CircuitHost>();
19+
private static readonly AsyncLocal<CircuitHost> _current = new AsyncLocal<CircuitHost>();
2020

2121
/// <summary>
2222
/// Gets the current <see cref="Circuit"/>, if any.
2323
/// </summary>
2424
public static CircuitHost Current => _current.Value;
2525

2626
/// <summary>
27-
/// Sets the current <see cref="Circuit"/>.
27+
/// Sets the current <see cref="Circuits.Circuit"/>.
2828
/// </summary>
29-
/// <param name="circuitHost">The <see cref="Circuit"/>.</param>
29+
/// <param name="circuitHost">The <see cref="Circuits.Circuit"/>.</param>
3030
/// <remarks>
3131
/// Calling <see cref="SetCurrentCircuitHost(CircuitHost)"/> will store the circuit
3232
/// and other related values such as the <see cref="IJSRuntime"/> and <see cref="Renderer"/>
3333
/// in the local execution context. Application code should not need to call this method,
34-
/// it is primarily used by the Server-Side Blazor infrastructure.
34+
/// it is primarily used by the Server-Side Components infrastructure.
3535
/// </remarks>
3636
public static void SetCurrentCircuitHost(CircuitHost circuitHost)
3737
{
38-
if (circuitHost == null)
39-
{
40-
throw new ArgumentNullException(nameof(circuitHost));
41-
}
42-
43-
_current.Value = circuitHost;
38+
_current.Value = circuitHost ?? throw new ArgumentNullException(nameof(circuitHost));
4439

4540
Microsoft.JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
4641
RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry);
@@ -134,6 +129,7 @@ await SynchronizationContext.Invoke(() =>
134129
public void Dispose()
135130
{
136131
Scope.Dispose();
132+
Renderer.Dispose();
137133
}
138134

139135
private void AssertInitialized()

src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/RemoteRenderer.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,10 @@ public override Task InvokeAsync(Func<Task> workItem)
120120
}
121121
}
122122

123-
/// <summary>
124-
/// Disposes the instance.
125-
/// </summary>
126-
public void Dispose()
123+
/// <inheritdoc />
124+
protected override void Dispose(bool disposing)
127125
{
126+
base.Dispose(true);
128127
_rendererRegistry.TryRemove(_id);
129128
}
130129

src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs

+45-8
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
1212
/// Provides mechanisms for rendering hierarchies of <see cref="IComponent"/> instances,
1313
/// dispatching events to them, and notifying when the user interface is being updated.
1414
/// </summary>
15-
public abstract class Renderer
15+
public abstract class Renderer : IDisposable
1616
{
1717
private readonly ComponentFactory _componentFactory;
18-
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
19-
private readonly Dictionary<int, ComponentState> _componentStateById
20-
= new Dictionary<int, ComponentState>();
21-
18+
private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
2219
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
23-
private bool _isBatchInProgress;
20+
private readonly Dictionary<int, EventHandlerInvoker> _eventBindings = new Dictionary<int, EventHandlerInvoker>();
2421

22+
private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
23+
private bool _isBatchInProgress;
2524
private int _lastEventHandlerId = 0;
26-
private readonly Dictionary<int, EventHandlerInvoker> _eventBindings = new Dictionary<int, EventHandlerInvoker>();
2725

2826
/// <summary>
2927
/// Constructs an instance of <see cref="Renderer"/>.
@@ -175,7 +173,7 @@ internal void AssignEventHandlerId(ref RenderTreeFrame frame)
175173

176174
if (frame.AttributeValue is MulticastDelegate @delegate)
177175
{
178-
_eventBindings.Add(id, new EventHandlerInvoker(@delegate));
176+
_eventBindings.Add(id, new EventHandlerInvoker(@delegate));
179177
}
180178

181179
frame = frame.WithAttributeEventHandlerId(id);
@@ -295,5 +293,44 @@ private void RemoveEventHandlerIds(ArrayRange<int> eventHandlerIds, Task afterTa
295293
RemoveEventHandlerIds(eventHandlerIdsClone, Task.CompletedTask));
296294
}
297295
}
296+
297+
/// <summary>
298+
/// Releases all resources currently used by this <see cref="Renderer"/> instance.
299+
/// </summary>
300+
/// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
301+
protected virtual void Dispose(bool disposing)
302+
{
303+
List<Exception> exceptions = null;
304+
305+
foreach (var componentState in _componentStateById.Values)
306+
{
307+
if (componentState.Component is IDisposable disposable)
308+
{
309+
try
310+
{
311+
disposable.Dispose();
312+
}
313+
catch (Exception exception)
314+
{
315+
// Capture exceptions thrown by individual components and rethrow as an aggregate.
316+
exceptions = exceptions ?? new List<Exception>();
317+
exceptions.Add(exception);
318+
}
319+
}
320+
}
321+
322+
if (exceptions != null)
323+
{
324+
throw new AggregateException(exceptions);
325+
}
326+
}
327+
328+
/// <summary>
329+
/// Releases all resources currently used by this <see cref="Renderer"/> instance.
330+
/// </summary>
331+
public void Dispose()
332+
{
333+
Dispose(disposing: true);
334+
}
298335
}
299336
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using Microsoft.AspNetCore.Components.Browser;
7+
using Microsoft.AspNetCore.Components.Browser.Rendering;
8+
using Microsoft.AspNetCore.SignalR;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.JSInterop;
11+
using Moq;
12+
using Xunit;
13+
14+
namespace Microsoft.AspNetCore.Components.Server.Circuits
15+
{
16+
public class CircuitHostTest
17+
{
18+
[Fact]
19+
public void Dispose_DisposesResources()
20+
{
21+
// Arrange
22+
var serviceScope = new Mock<IServiceScope>();
23+
var clientProxy = Mock.Of<IClientProxy>();
24+
var renderRegistry = new RendererRegistry();
25+
var jsRuntime = Mock.Of<IJSRuntime>();
26+
var syncContext = new CircuitSynchronizationContext();
27+
28+
var remoteRenderer = new TestRemoteRenderer(
29+
Mock.Of<IServiceProvider>(),
30+
renderRegistry,
31+
jsRuntime,
32+
clientProxy,
33+
syncContext);
34+
35+
var circuitHost = new CircuitHost(serviceScope.Object, clientProxy, renderRegistry, remoteRenderer, configure: _ => { }, jsRuntime: jsRuntime, synchronizationContext: syncContext);
36+
37+
// Act
38+
circuitHost.Dispose();
39+
40+
// Assert
41+
serviceScope.Verify(s => s.Dispose(), Times.Once());
42+
Assert.True(remoteRenderer.Disposed);
43+
}
44+
45+
private class TestRemoteRenderer : RemoteRenderer
46+
{
47+
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client, SynchronizationContext syncContext)
48+
: base(serviceProvider, rendererRegistry, jsRuntime, client, syncContext)
49+
{
50+
}
51+
52+
public bool Disposed { get; set; }
53+
54+
protected override void Dispose(bool disposing)
55+
{
56+
base.Dispose(disposing);
57+
Disposed = true;
58+
}
59+
}
60+
}
61+
}

src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Microsoft.AspNetCore.Components.Server.Test.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netcoreapp3.0</TargetFramework>
@@ -7,6 +7,7 @@
77

88
<ItemGroup>
99
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
10+
<PackageReference Include="Moq" Version="4.10.0" />
1011
<PackageReference Include="xunit" Version="2.3.1" />
1112
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
1213
</ItemGroup>

0 commit comments

Comments
 (0)