diff --git a/src/Components/.editorconfig b/src/Components/.editorconfig
index 0d238f8e41d1..f9d02415ce33 100644
--- a/src/Components/.editorconfig
+++ b/src/Components/.editorconfig
@@ -25,3 +25,31 @@ indent_size = 2
[*.{xml,csproj,config,*proj,targets,props}]
indent_size = 2
+
+# Dotnet code style settings:
+[*.cs]
+# Sort using and Import directives with System.* appearing first
+dotnet_sort_system_directives_first = true
+
+# Don't use this. qualifier
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+
+# use int x = .. over Int32
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+
+# use int.MaxValue over Int32.MaxValue
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Require var all the time.
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+
+# Newline settings
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
diff --git a/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Rendering/WebAssemblyRenderer.cs b/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Rendering/WebAssemblyRenderer.cs
index e6b4de5dc685..93941e4d3434 100644
--- a/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Rendering/WebAssemblyRenderer.cs
+++ b/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Rendering/WebAssemblyRenderer.cs
@@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
/// Provides mechanisms for rendering instances in a
/// web browser, dispatching events to them, and refreshing the UI as required.
///
- public class WebAssemblyRenderer : Renderer, IDisposable
+ public class WebAssemblyRenderer : Renderer
{
private readonly int _webAssemblyRendererId;
@@ -71,11 +71,10 @@ public void AddComponent(Type componentType, string domElementSelector)
RenderRootComponent(componentId);
}
- ///
- /// Disposes the instance.
- ///
- public void Dispose()
+ ///
+ protected override void Dispose(bool disposing)
{
+ base.Dispose(disposing);
RendererRegistry.Current.TryRemove(_webAssemblyRendererId);
}
diff --git a/src/Components/src/Microsoft.AspNetCore.Components.Browser/Properties/AssemblyInfo.cs b/src/Components/src/Microsoft.AspNetCore.Components.Browser/Properties/AssemblyInfo.cs
index b513292596bc..a311581a181c 100644
--- a/src/Components/src/Microsoft.AspNetCore.Components.Browser/Properties/AssemblyInfo.cs
+++ b/src/Components/src/Microsoft.AspNetCore.Components.Browser/Properties/AssemblyInfo.cs
@@ -2,3 +2,5 @@
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/CircuitHost.cs b/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/CircuitHost.cs
index 8c483daf8ba4..ea62df438c10 100644
--- a/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/CircuitHost.cs
+++ b/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/CircuitHost.cs
@@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class CircuitHost : IDisposable
{
- private static AsyncLocal _current = new AsyncLocal();
+ private static readonly AsyncLocal _current = new AsyncLocal();
///
/// Gets the current , if any.
@@ -24,23 +24,18 @@ internal class CircuitHost : IDisposable
public static CircuitHost Current => _current.Value;
///
- /// Sets the current .
+ /// Sets the current .
///
- /// The .
+ /// The .
///
/// Calling will store the circuit
/// and other related values such as the and
/// in the local execution context. Application code should not need to call this method,
- /// it is primarily used by the Server-Side Blazor infrastructure.
+ /// it is primarily used by the Server-Side Components infrastructure.
///
public static void SetCurrentCircuitHost(CircuitHost circuitHost)
{
- if (circuitHost == null)
- {
- throw new ArgumentNullException(nameof(circuitHost));
- }
-
- _current.Value = circuitHost;
+ _current.Value = circuitHost ?? throw new ArgumentNullException(nameof(circuitHost));
Microsoft.JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry);
@@ -134,6 +129,7 @@ await SynchronizationContext.Invoke(() =>
public void Dispose()
{
Scope.Dispose();
+ Renderer.Dispose();
}
private void AssertInitialized()
diff --git a/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/RemoteRenderer.cs b/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/RemoteRenderer.cs
index e60b9643e16a..b81673f8ca9c 100644
--- a/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/RemoteRenderer.cs
+++ b/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/RemoteRenderer.cs
@@ -120,11 +120,10 @@ public override Task InvokeAsync(Func workItem)
}
}
- ///
- /// Disposes the instance.
- ///
- public void Dispose()
+ ///
+ protected override void Dispose(bool disposing)
{
+ base.Dispose(true);
_rendererRegistry.TryRemove(_id);
}
diff --git a/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs
index ff8e2b0f8e48..3dde251101b8 100644
--- a/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs
+++ b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs
@@ -12,18 +12,16 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// Provides mechanisms for rendering hierarchies of instances,
/// dispatching events to them, and notifying when the user interface is being updated.
///
- public abstract class Renderer
+ public abstract class Renderer : IDisposable
{
private readonly ComponentFactory _componentFactory;
- private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
- private readonly Dictionary _componentStateById
- = new Dictionary();
-
+ private readonly Dictionary _componentStateById = new Dictionary();
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
- private bool _isBatchInProgress;
+ private readonly Dictionary _eventBindings = new Dictionary();
+ private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it
+ private bool _isBatchInProgress;
private int _lastEventHandlerId = 0;
- private readonly Dictionary _eventBindings = new Dictionary();
///
/// Constructs an instance of .
@@ -175,7 +173,7 @@ internal void AssignEventHandlerId(ref RenderTreeFrame frame)
if (frame.AttributeValue is MulticastDelegate @delegate)
{
- _eventBindings.Add(id, new EventHandlerInvoker(@delegate));
+ _eventBindings.Add(id, new EventHandlerInvoker(@delegate));
}
frame = frame.WithAttributeEventHandlerId(id);
@@ -295,5 +293,44 @@ private void RemoveEventHandlerIds(ArrayRange eventHandlerIds, Task afterTa
RemoveEventHandlerIds(eventHandlerIdsClone, Task.CompletedTask));
}
}
+
+ ///
+ /// Releases all resources currently used by this instance.
+ ///
+ /// if this method is being invoked by , otherwise .
+ protected virtual void Dispose(bool disposing)
+ {
+ List exceptions = null;
+
+ foreach (var componentState in _componentStateById.Values)
+ {
+ if (componentState.Component is IDisposable disposable)
+ {
+ try
+ {
+ disposable.Dispose();
+ }
+ catch (Exception exception)
+ {
+ // Capture exceptions thrown by individual components and rethrow as an aggregate.
+ exceptions = exceptions ?? new List();
+ exceptions.Add(exception);
+ }
+ }
+ }
+
+ if (exceptions != null)
+ {
+ throw new AggregateException(exceptions);
+ }
+ }
+
+ ///
+ /// Releases all resources currently used by this instance.
+ ///
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ }
}
}
diff --git a/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Circuits/CircuitHostTest.cs b/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Circuits/CircuitHostTest.cs
new file mode 100644
index 000000000000..27a35bfa7328
--- /dev/null
+++ b/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Circuits/CircuitHostTest.cs
@@ -0,0 +1,61 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using Microsoft.AspNetCore.Components.Browser;
+using Microsoft.AspNetCore.Components.Browser.Rendering;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.JSInterop;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Components.Server.Circuits
+{
+ public class CircuitHostTest
+ {
+ [Fact]
+ public void Dispose_DisposesResources()
+ {
+ // Arrange
+ var serviceScope = new Mock();
+ var clientProxy = Mock.Of();
+ var renderRegistry = new RendererRegistry();
+ var jsRuntime = Mock.Of();
+ var syncContext = new CircuitSynchronizationContext();
+
+ var remoteRenderer = new TestRemoteRenderer(
+ Mock.Of(),
+ renderRegistry,
+ jsRuntime,
+ clientProxy,
+ syncContext);
+
+ var circuitHost = new CircuitHost(serviceScope.Object, clientProxy, renderRegistry, remoteRenderer, configure: _ => { }, jsRuntime: jsRuntime, synchronizationContext: syncContext);
+
+ // Act
+ circuitHost.Dispose();
+
+ // Assert
+ serviceScope.Verify(s => s.Dispose(), Times.Once());
+ Assert.True(remoteRenderer.Disposed);
+ }
+
+ private class TestRemoteRenderer : RemoteRenderer
+ {
+ public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client, SynchronizationContext syncContext)
+ : base(serviceProvider, rendererRegistry, jsRuntime, client, syncContext)
+ {
+ }
+
+ public bool Disposed { get; set; }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ Disposed = true;
+ }
+ }
+ }
+}
diff --git a/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Microsoft.AspNetCore.Components.Server.Test.csproj b/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Microsoft.AspNetCore.Components.Server.Test.csproj
index 152cb7df884c..204aecef3783 100644
--- a/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Microsoft.AspNetCore.Components.Server.Test.csproj
+++ b/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Microsoft.AspNetCore.Components.Server.Test.csproj
@@ -1,4 +1,4 @@
-
+
netcoreapp3.0
@@ -7,6 +7,7 @@
+
diff --git a/src/Components/test/Microsoft.AspNetCore.Components.Test/RendererTest.cs b/src/Components/test/Microsoft.AspNetCore.Components.Test/RendererTest.cs
index 5fe8b7909753..57eb16707d78 100644
--- a/src/Components/test/Microsoft.AspNetCore.Components.Test/RendererTest.cs
+++ b/src/Components/test/Microsoft.AspNetCore.Components.Test/RendererTest.cs
@@ -5,7 +5,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
@@ -1139,6 +1138,78 @@ public async Task CanTriggerEventHandlerDisposedInEarlierPendingBatch()
Assert.Equal(2, numEventsFired);
}
+ [Fact]
+ public void DisposingRenderer_DisposesTopLevelComponents()
+ {
+ // Arrange
+ var renderer = new TestRenderer();
+ var component = new DisposableComponent();
+ renderer.AssignRootComponentId(component);
+
+ // Act
+ renderer.Dispose();
+
+ // Assert
+ Assert.True(component.Disposed);
+ }
+
+ [Fact]
+ public void DisposingRenderer_DisposesNestedComponents()
+ {
+ // Arrange
+ var renderer = new TestRenderer();
+ var component = new TestComponent(builder =>
+ {
+ builder.AddContent(0, "Hello");
+ builder.OpenComponent(1);
+ builder.CloseComponent();
+ });
+ var componentId = renderer.AssignRootComponentId(component);
+ component.TriggerRender();
+ var batch = renderer.Batches.Single();
+ var componentFrame = batch.ReferenceFrames
+ .Single(frame => frame.FrameType == RenderTreeFrameType.Component);
+ var nestedComponent = Assert.IsType(componentFrame.Component);
+
+ // Act
+ renderer.Dispose();
+
+ // Assert
+ Assert.True(component.Disposed);
+ Assert.True(nestedComponent.Disposed);
+ }
+
+ [Fact]
+ public void DisposingRenderer_CapturesExceptionsFromAllRegisteredComponents()
+ {
+ // Arrange
+ var renderer = new TestRenderer();
+ var exception1 = new Exception();
+ var exception2 = new Exception();
+ var component = new TestComponent(builder =>
+ {
+ builder.AddContent(0, "Hello");
+ builder.OpenComponent(1);
+ builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception1));
+ builder.CloseComponent();
+
+ builder.OpenComponent(2);
+ builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception2));
+ builder.CloseComponent();
+ });
+ var componentId = renderer.AssignRootComponentId(component);
+ component.TriggerRender();
+
+ // Act &A Assert
+ var aggregate = Assert.Throws(renderer.Dispose);
+
+ // All components must be disposed even if some throw as part of being diposed.
+ Assert.True(component.Disposed);
+ Assert.Equal(2, aggregate.InnerExceptions.Count);
+ Assert.Contains(exception1, aggregate.InnerExceptions);
+ Assert.Contains(exception2, aggregate.InnerExceptions);
+ }
+
private class NoOpRenderer : Renderer
{
public NoOpRenderer() : base(new TestServiceProvider())
@@ -1152,7 +1223,7 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> Task.CompletedTask;
}
- private class TestComponent : IComponent
+ private class TestComponent : IComponent, IDisposable
{
private RenderHandle _renderHandle;
private RenderFragment _renderFragment;
@@ -1172,6 +1243,10 @@ public void SetParameters(ParameterCollection parameters)
public void TriggerRender()
=> _renderHandle.Render(_renderFragment);
+
+ public bool Disposed { get; private set; }
+
+ void IDisposable.Dispose() => Disposed = true;
}
private class MessageComponent : AutoRenderComponent
@@ -1398,6 +1473,24 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
}
}
+ private class DisposableComponent : AutoRenderComponent, IDisposable
+ {
+ public bool Disposed { get; private set; }
+
+ [Parameter]
+ public Action DisposeAction { get; private set; }
+
+ public void Dispose()
+ {
+ Disposed = true;
+ DisposeAction?.Invoke();
+ }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ }
+ }
+
class TestAsyncRenderer : TestRenderer
{
public Task NextUpdateDisplayReturnTask { get; set; }