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; }