From e312d641944d9d66d081483a593d25212b19277e Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 23 Jan 2019 16:45:56 -0800 Subject: [PATCH] [Design]: Introduce CircuitHandler to handle circuit lifetime events Partial fix to https://github.com/aspnet/AspNetCore/issues/6353 --- ...rComponentsApplicationBuilderExtensions.cs | 2 +- ...ServerSideComponentsApplicationBuilder.cs} | 4 +- src/Components/Server/src/Circuits/Circuit.cs | 24 +-- .../Server/src/Circuits/CircuitHandler.cs | 81 ++++++++++ .../Server/src/Circuits/CircuitHost.cs | 65 +++++--- .../src/Circuits/DefaultCircuitFactory.cs | 10 +- .../Circuits/DefaultCircuitFactoryOptions.cs | 12 +- .../Server/src/Circuits/RemoteUriHelper.cs | 2 +- .../src/{BlazorHub.cs => ComponentsHub.cs} | 30 ++-- ...orComponentsServiceCollectionExtensions.cs | 7 +- ...rosoft.AspNetCore.Components.Server.csproj | 2 +- .../Server/src/Properties/AssemblyInfo.cs | 2 + .../Server/test/Circuits/CircuitHostTest.cs | 142 ++++++++++++++++-- ....AspNetCore.Components.Server.Tests.csproj | 6 +- .../LoggingCircuitHandler.cs | 69 +++++++++ .../ComponentsApp.Server/Startup.cs | 2 + .../test/testassets/TestServer/Startup.cs | 2 +- 17 files changed, 376 insertions(+), 86 deletions(-) rename src/Components/Server/src/Builder/{ServerSideBlazorApplicationBuilder.cs => ServerSideComponentsApplicationBuilder.cs} (86%) create mode 100644 src/Components/Server/src/Circuits/CircuitHandler.cs rename src/Components/Server/src/{BlazorHub.cs => ComponentsHub.cs} (85%) create mode 100644 src/Components/test/testassets/ComponentsApp.Server/LoggingCircuitHandler.cs diff --git a/src/Components/Server/src/Builder/RazorComponentsApplicationBuilderExtensions.cs b/src/Components/Server/src/Builder/RazorComponentsApplicationBuilderExtensions.cs index b20efd7650..f3a19d4212 100644 --- a/src/Components/Server/src/Builder/RazorComponentsApplicationBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/RazorComponentsApplicationBuilderExtensions.cs @@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Builder // add SignalR and BlazorHub automatically. if (options.UseSignalRWithBlazorHub) { - builder.UseSignalR(route => route.MapHub(BlazorHub.DefaultPath)); + builder.UseSignalR(route => route.MapHub(ComponentsHub.DefaultPath)); } // Use embedded static content for /_framework diff --git a/src/Components/Server/src/Builder/ServerSideBlazorApplicationBuilder.cs b/src/Components/Server/src/Builder/ServerSideComponentsApplicationBuilder.cs similarity index 86% rename from src/Components/Server/src/Builder/ServerSideBlazorApplicationBuilder.cs rename to src/Components/Server/src/Builder/ServerSideComponentsApplicationBuilder.cs index 3cf0807fb3..3f42f18933 100644 --- a/src/Components/Server/src/Builder/ServerSideBlazorApplicationBuilder.cs +++ b/src/Components/Server/src/Builder/ServerSideComponentsApplicationBuilder.cs @@ -7,9 +7,9 @@ using Microsoft.AspNetCore.Components.Builder; namespace Microsoft.AspNetCore.Components.Hosting { - internal class ServerSideBlazorApplicationBuilder : IComponentsApplicationBuilder + internal class ServerSideComponentsApplicationBuilder : IComponentsApplicationBuilder { - public ServerSideBlazorApplicationBuilder(IServiceProvider services) + public ServerSideComponentsApplicationBuilder(IServiceProvider services) { Services = services; Entries = new List<(Type componentType, string domElementSelector)>(); diff --git a/src/Components/Server/src/Circuits/Circuit.cs b/src/Components/Server/src/Circuits/Circuit.cs index 643683e351..536fb86f93 100644 --- a/src/Components/Server/src/Circuits/Circuit.cs +++ b/src/Components/Server/src/Circuits/Circuit.cs @@ -1,35 +1,23 @@ // 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 Microsoft.JSInterop; - namespace Microsoft.AspNetCore.Components.Server.Circuits { /// - /// Represents an active connection between a Blazor server and a client. + /// Represents a link between a ASP.NET Core Component on the server and a client. /// - public class Circuit + public sealed class Circuit { - /// - /// Gets the current . - /// - public static Circuit Current => CircuitHost.Current?.Circuit; + private readonly CircuitHost _circuitHost; internal Circuit(CircuitHost circuitHost) { - JSRuntime = circuitHost.JSRuntime; - Services = circuitHost.Services; + _circuitHost = circuitHost; } /// - /// Gets the associated with this circuit. + /// Gets the identifier for the . /// - public IJSRuntime JSRuntime { get; } - - /// - /// Gets the associated with this circuit. - /// - public IServiceProvider Services { get; } + public string Id => _circuitHost.CircuitId; } } diff --git a/src/Components/Server/src/Circuits/CircuitHandler.cs b/src/Components/Server/src/Circuits/CircuitHandler.cs new file mode 100644 index 0000000000..1f8748be7b --- /dev/null +++ b/src/Components/Server/src/Circuits/CircuitHandler.cs @@ -0,0 +1,81 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components.Server.Circuits +{ + /// + /// A allows running code during specific lifetime events of a . + /// + /// + /// is invoked after an initial circuit to the client + /// has been established. + /// + /// + /// is invoked immediately after the completion of + /// . In addition, the method is invoked each time a connection is re-established + /// with a client after it's been dropped. is invoked each time a connection + /// is dropped. + /// + /// + /// is invoked prior to the server evicting the circuit to the client. + /// Application users may use this event to save state for a client that can be later rehydrated. + /// + /// + ///
    + ///
+ public abstract class CircuitHandler + { + /// + /// Gets the execution order for the current instance of . + /// + /// When multiple instances are registered, the + /// property is used to determine the order in which instances are executed. When two handlers + /// have the same value for , their execution order is non-deterministic. + /// + /// + /// + /// Defaults to 0. + /// + public virtual int Order => 0; + + /// + /// Invoked when a new circuit was established. + /// + /// The . + /// The . + /// that represents the asynchronous execution operation. + public virtual Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask; + + /// + /// Invoked when a connection to the client was established. + /// + /// This method is executed once initially after + /// and once each for each reconnect during the lifetime of a circuit. + /// + /// + /// The . + /// The . + /// that represents the asynchronous execution operation. + public virtual Task OnConnectionUpAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask; + + /// + /// Invoked a connection to the client using was dropped. + /// + /// The . + /// The . + /// that represents the asynchronous execution operation. + public virtual Task OnConnectionDownAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask; + + + /// + /// Invoked when a new circuit is being discarded. + /// + /// The . + /// The . + /// that represents the asynchronous execution operation. + public virtual Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask; + } +} diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index 20bc7e6a21..02c771013c 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -14,9 +14,14 @@ using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Server.Circuits { - internal class CircuitHost : IDisposable + internal class CircuitHost : IAsyncDisposable { private static readonly AsyncLocal _current = new AsyncLocal(); + private readonly IServiceScope _scope; + private readonly CircuitHandler[] _circuitHandlers; + private bool _initialized; + + private Action _configure; /// /// Gets the current , if any. @@ -37,15 +42,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { _current.Value = circuitHost ?? throw new ArgumentNullException(nameof(circuitHost)); - Microsoft.JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime); + JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime); RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry); } public event UnhandledExceptionEventHandler UnhandledException; - private bool _isInitialized; - private Action _configure; - public CircuitHost( IServiceScope scope, IClientProxy client, @@ -53,9 +55,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits RemoteRenderer renderer, Action configure, IJSRuntime jsRuntime, - CircuitSynchronizationContext synchronizationContext) + CircuitSynchronizationContext synchronizationContext, + CircuitHandler[] circuitHandlers) { - Scope = scope ?? throw new ArgumentNullException(nameof(scope)); + _scope = scope ?? throw new ArgumentNullException(nameof(scope)); Client = client ?? throw new ArgumentNullException(nameof(client)); RendererRegistry = rendererRegistry ?? throw new ArgumentNullException(nameof(rendererRegistry)); Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); @@ -66,11 +69,14 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits Services = scope.ServiceProvider; Circuit = new Circuit(this); + _circuitHandlers = circuitHandlers; Renderer.UnhandledException += Renderer_UnhandledException; SynchronizationContext.UnhandledException += SynchronizationContext_UnhandledException; } + public string CircuitId { get; } = Guid.NewGuid().ToString(); + public Circuit Circuit { get; } public IClientProxy Client { get; } @@ -81,30 +87,40 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public RendererRegistry RendererRegistry { get; } - public IServiceScope Scope { get; } - public IServiceProvider Services { get; } public CircuitSynchronizationContext SynchronizationContext { get; } - public async Task InitializeAsync() + public CancellationToken ConnectionAborted { get; } + + public async Task InitializeAsync(CancellationToken cancellationToken) { - await SynchronizationContext.Invoke(() => + await SynchronizationContext.InvokeAsync(async () => { SetCurrentCircuitHost(this); - var builder = new ServerSideBlazorApplicationBuilder(Services); + var builder = new ServerSideComponentsApplicationBuilder(Services); _configure(builder); for (var i = 0; i < builder.Entries.Count; i++) { - var entry = builder.Entries[i]; - Renderer.AddComponent(entry.componentType, entry.domElementSelector); + var (componentType, domElementSelector) = builder.Entries[i]; + Renderer.AddComponent(componentType, domElementSelector); + } + + for (var i = 0; i < _circuitHandlers.Length; i++) + { + await _circuitHandlers[i].OnCircuitOpenedAsync(Circuit, cancellationToken); + } + + for (var i = 0; i < _circuitHandlers.Length; i++) + { + await _circuitHandlers[i].OnConnectionUpAsync(Circuit, cancellationToken); } }); - _isInitialized = true; + _initialized = true; } public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) @@ -126,15 +142,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } } - public void Dispose() + public async ValueTask DisposeAsync() { - Scope.Dispose(); + await SynchronizationContext.InvokeAsync(async () => + { + for (var i = 0; i < _circuitHandlers.Length; i++) + { + await _circuitHandlers[i].OnConnectionDownAsync(Circuit, default); + } + + for (var i = 0; i < _circuitHandlers.Length; i++) + { + await _circuitHandlers[i].OnCircuitClosedAsync(Circuit, default); + } + }); + + _scope.Dispose(); Renderer.Dispose(); } private void AssertInitialized() { - if (!_isInitialized) + if (!_initialized) { throw new InvalidOperationException("Something is calling into the circuit before Initialize() completes"); } diff --git a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs index 11d0b740e7..3a8b1b56ce 100644 --- a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs +++ b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using Microsoft.AspNetCore.Components.Browser; using Microsoft.AspNetCore.Components.Browser.Rendering; using Microsoft.AspNetCore.Http; @@ -33,7 +34,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { if (!_options.StartupActions.TryGetValue(httpContext.Request.Path, out var config)) { - var message = $"Could not find a Blazor startup action for request path {httpContext.Request.Path}"; + var message = $"Could not find an ASP.NET Core Components startup action for request path '{httpContext.Request.Path}'."; throw new InvalidOperationException(message); } @@ -43,6 +44,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits var synchronizationContext = new CircuitSynchronizationContext(); var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client, synchronizationContext); + var circuitHandlers = scope.ServiceProvider.GetServices() + .OrderBy(h => h.Order) + .ToArray(); + var circuitHost = new CircuitHost( scope, client, @@ -50,7 +55,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits renderer, config, jsRuntime, - synchronizationContext); + synchronizationContext, + circuitHandlers); // Initialize per-circuit data that services need (circuitHost.Services.GetRequiredService() as DefaultJSRuntimeAccessor).JSRuntime = jsRuntime; diff --git a/src/Components/Server/src/Circuits/DefaultCircuitFactoryOptions.cs b/src/Components/Server/src/Circuits/DefaultCircuitFactoryOptions.cs index 6d7bf1178e..1ae5a9a982 100644 --- a/src/Components/Server/src/Circuits/DefaultCircuitFactoryOptions.cs +++ b/src/Components/Server/src/Circuits/DefaultCircuitFactoryOptions.cs @@ -6,17 +6,13 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Components.Builder; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Components.Server.Circuits +namespace Microsoft.AspNetCore.Components.Server { - internal class DefaultCircuitFactoryOptions + public class DefaultCircuitFactoryOptions { // During the DI configuration phase, we use Configure(...) // callbacks to build up this dictionary mapping paths to startup actions - public Dictionary> StartupActions { get; } - - public DefaultCircuitFactoryOptions() - { - StartupActions = new Dictionary>(); - } + internal Dictionary> StartupActions { get; } + = new Dictionary>(); } } diff --git a/src/Components/Server/src/Circuits/RemoteUriHelper.cs b/src/Components/Server/src/Circuits/RemoteUriHelper.cs index a3a0d0d23c..37bb8e79cc 100644 --- a/src/Components/Server/src/Circuits/RemoteUriHelper.cs +++ b/src/Components/Server/src/Circuits/RemoteUriHelper.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits [JSInvokable(nameof(NotifyLocationChanged))] public static void NotifyLocationChanged(string uriAbsolute) { - var circuit = Circuit.Current; + var circuit = CircuitHost.Current; if (circuit == null) { var message = $"{nameof(NotifyLocationChanged)} called without a circuit."; diff --git a/src/Components/Server/src/BlazorHub.cs b/src/Components/Server/src/ComponentsHub.cs similarity index 85% rename from src/Components/Server/src/BlazorHub.cs rename to src/Components/Server/src/ComponentsHub.cs index e2a0b071dd..f56971f1cd 100644 --- a/src/Components/Server/src/BlazorHub.cs +++ b/src/Components/Server/src/ComponentsHub.cs @@ -13,9 +13,9 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Server { /// - /// A SignalR hub that accepts connections to a Server-Side Blazor app. + /// A SignalR hub that accepts connections to a ASP.NET Core Components WebApp. /// - public sealed class BlazorHub : Hub + public sealed class ComponentsHub : Hub { private static readonly object CircuitKey = new object(); private readonly CircuitFactory _circuitFactory; @@ -25,32 +25,32 @@ namespace Microsoft.AspNetCore.Components.Server /// Intended for framework use only. Applications should not instantiate /// this class directly. /// - public BlazorHub( - ILogger logger, - IServiceProvider services) + public ComponentsHub(IServiceProvider services, ILogger logger) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _circuitFactory = services.GetRequiredService(); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Gets the default endpoint path for incoming connections. /// - public static PathString DefaultPath => "/_blazor"; + public static PathString DefaultPath { get; } = "/_blazor"; - private CircuitHost CircuitHost + /// + /// For unit testing only. + /// + internal CircuitHost CircuitHost { get => (CircuitHost)Context.Items[CircuitKey]; - set => Context.Items[CircuitKey] = value; + private set => Context.Items[CircuitKey] = value; } /// /// Intended for framework use only. Applications should not call this method directly. /// - public override Task OnDisconnectedAsync(Exception exception) + public override async Task OnDisconnectedAsync(Exception exception) { - CircuitHost.Dispose(); - return base.OnDisconnectedAsync(exception); + await CircuitHost.DisposeAsync(); } /// @@ -64,9 +64,9 @@ namespace Microsoft.AspNetCore.Components.Server var uriHelper = (RemoteUriHelper)circuitHost.Services.GetRequiredService(); uriHelper.Initialize(uriAbsolute, baseUriAbsolute); - // If initialization fails, this will throw. The caller will explode if they - // try to call into any interop API. - await circuitHost.InitializeAsync(); + // If initialization fails, this will throw. The caller will fail if they try to call into any interop API. + await circuitHost.InitializeAsync(Context.ConnectionAborted); + CircuitHost = circuitHost; } diff --git a/src/Components/Server/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index c563ceef93..51c29bbefe 100644 --- a/src/Components/Server/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Services; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.JSInterop; namespace Microsoft.Extensions.DependencyInjection { @@ -74,7 +73,7 @@ namespace Microsoft.Extensions.DependencyInjection // TStartup's Configure method". services.Configure(circuitFactoryOptions => { - var endpoint = BlazorHub.DefaultPath; // TODO: allow configuring this + var endpoint = ComponentsHub.DefaultPath; // TODO: allow configuring this if (circuitFactoryOptions.StartupActions.ContainsKey(endpoint)) { throw new InvalidOperationException( @@ -99,9 +98,9 @@ namespace Microsoft.Extensions.DependencyInjection // Components entrypoints, this lot is the same and repeated registrations are a no-op. services.TryAddSingleton(); services.TryAddScoped(); - services.TryAddScoped(s => s.GetRequiredService().Circuit); + services.TryAddScoped(s => s.GetRequiredService().Circuit); services.TryAddScoped(); - services.TryAddScoped(s => s.GetRequiredService().JSRuntime); + services.TryAddScoped(s => s.GetRequiredService().JSRuntime); services.TryAddScoped(); // We've discussed with the SignalR team and believe it's OK to have repeated diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index d4d03b45bd..3fefa1ef46 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 diff --git a/src/Components/Server/src/Properties/AssemblyInfo.cs b/src/Components/Server/src/Properties/AssemblyInfo.cs index 3c7cfc70ee..29d50b9636 100644 --- a/src/Components/Server/src/Properties/AssemblyInfo.cs +++ b/src/Components/Server/src/Properties/AssemblyInfo.cs @@ -2,3 +2,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Cli, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs index 27a35bfa73..ae25162015 100644 --- a/src/Components/Server/test/Circuits/CircuitHostTest.cs +++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs @@ -3,6 +3,7 @@ using System; using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Browser; using Microsoft.AspNetCore.Components.Browser.Rendering; using Microsoft.AspNetCore.SignalR; @@ -16,30 +17,143 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public class CircuitHostTest { [Fact] - public void Dispose_DisposesResources() + public async Task DisposeAsync_DisposesResources() { // Arrange var serviceScope = new Mock(); + var remoteRenderer = GetRemoteRenderer(); + var circuitHost = GetCircuitHost( + serviceScope.Object, + remoteRenderer); + + // Act + await circuitHost.DisposeAsync(); + + // Assert + serviceScope.Verify(s => s.Dispose(), Times.Once()); + Assert.True(remoteRenderer.Disposed); + } + + [Fact] + public async Task InitializeAsync_InvokesHandlers() + { + // Arrange + var cancellationToken = new CancellationToken(); + var handler1 = new Mock(MockBehavior.Strict); + var handler2 = new Mock(MockBehavior.Strict); + var sequence = new MockSequence(); + + handler1 + .InSequence(sequence) + .Setup(h => h.OnCircuitOpenedAsync(It.IsAny(), cancellationToken)) + .Returns(Task.CompletedTask) + .Verifiable(); + + handler2 + .InSequence(sequence) + .Setup(h => h.OnCircuitOpenedAsync(It.IsAny(), cancellationToken)) + .Returns(Task.CompletedTask) + .Verifiable(); + + handler1 + .InSequence(sequence) + .Setup(h => h.OnConnectionUpAsync(It.IsAny(), cancellationToken)) + .Returns(Task.CompletedTask) + .Verifiable(); + + handler2 + .InSequence(sequence) + .Setup(h => h.OnConnectionUpAsync(It.IsAny(), cancellationToken)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var circuitHost = GetCircuitHost(handlers: new[] { handler1.Object, handler2.Object }); + + // Act + await circuitHost.InitializeAsync(cancellationToken); + + // Assert + handler1.VerifyAll(); + handler2.VerifyAll(); + } + + [Fact] + public async Task DisposeAsync_InvokesCircuitHandler() + { + // Arrange + var cancellationToken = new CancellationToken(); + var handler1 = new Mock(MockBehavior.Strict); + var handler2 = new Mock(MockBehavior.Strict); + var sequence = new MockSequence(); + + handler1 + .InSequence(sequence) + .Setup(h => h.OnConnectionDownAsync(It.IsAny(), cancellationToken)) + .Returns(Task.CompletedTask) + .Verifiable(); + + handler2 + .InSequence(sequence) + .Setup(h => h.OnConnectionDownAsync(It.IsAny(), cancellationToken)) + .Returns(Task.CompletedTask) + .Verifiable(); + + handler1 + .InSequence(sequence) + .Setup(h => h.OnCircuitClosedAsync(It.IsAny(), cancellationToken)) + .Returns(Task.CompletedTask) + .Verifiable(); + + handler2 + .InSequence(sequence) + .Setup(h => h.OnCircuitClosedAsync(It.IsAny(), cancellationToken)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var circuitHost = GetCircuitHost(handlers: new[] { handler1.Object, handler2.Object }); + + // Act + await circuitHost.DisposeAsync(); + + // Assert + handler1.VerifyAll(); + handler2.VerifyAll(); + } + + private static CircuitHost GetCircuitHost( + IServiceScope serviceScope = null, + RemoteRenderer remoteRenderer = null, + CircuitHandler[] handlers = null) + { + serviceScope = serviceScope ?? Mock.Of(); var clientProxy = Mock.Of(); var renderRegistry = new RendererRegistry(); var jsRuntime = Mock.Of(); var syncContext = new CircuitSynchronizationContext(); - var remoteRenderer = new TestRemoteRenderer( - Mock.Of(), - renderRegistry, - jsRuntime, + remoteRenderer = remoteRenderer ?? GetRemoteRenderer(); + handlers = handlers ?? Array.Empty(); + + return new CircuitHost( + serviceScope, clientProxy, - syncContext); + renderRegistry, + remoteRenderer, + configure: _ => { }, + jsRuntime: jsRuntime, + synchronizationContext: + syncContext, + handlers); + } - 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 static TestRemoteRenderer GetRemoteRenderer() + { + return new TestRemoteRenderer( + Mock.Of(), + new RendererRegistry(), + Mock.Of(), + Mock.Of(), + new CircuitSynchronizationContext()); } private class TestRemoteRenderer : RemoteRenderer diff --git a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj index 06cf534689..895e168b2a 100644 --- a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj +++ b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -8,4 +8,8 @@ + + + + diff --git a/src/Components/test/testassets/ComponentsApp.Server/LoggingCircuitHandler.cs b/src/Components/test/testassets/ComponentsApp.Server/LoggingCircuitHandler.cs new file mode 100644 index 0000000000..1a607aac67 --- /dev/null +++ b/src/Components/test/testassets/ComponentsApp.Server/LoggingCircuitHandler.cs @@ -0,0 +1,69 @@ +// 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 System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Server.Circuits; +using Microsoft.Extensions.Logging; + +namespace ComponentsApp.Server +{ + internal class LoggingCircuitHandler : CircuitHandler + { + private readonly ILogger _logger; + private static Action _circuitOpened; + private static Action _connectionUp; + private static Action _connectionDown; + private static Action _circuitClosed; + + public LoggingCircuitHandler(ILogger logger) + { + _logger = logger; + + _circuitOpened = LoggerMessage.Define( + logLevel: LogLevel.Information, + 1, + formatString: "Circuit opened for {circuitId}."); + + _connectionUp = LoggerMessage.Define( + logLevel: LogLevel.Information, + 2, + formatString: "Connection up for {circuitId}."); + + _connectionDown = LoggerMessage.Define( + logLevel: LogLevel.Information, + 3, + formatString: "Connection down for {circuitId}."); + + _circuitClosed = LoggerMessage.Define( + logLevel: LogLevel.Information, + 3, + formatString: "Circuit closed for {circuitId}."); + } + + public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cts) + { + _circuitOpened(_logger, circuit.Id, null); + return base.OnCircuitOpenedAsync(circuit, cts); + } + + public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cts) + { + _connectionUp(_logger, circuit.Id, null); + return base.OnConnectionUpAsync(circuit, cts); + } + + public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cts) + { + _connectionDown(_logger, circuit.Id, null); + return base.OnConnectionDownAsync(circuit, cts); + } + + public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cts) + { + _circuitClosed(_logger, circuit.Id, null); + return base.OnCircuitClosedAsync(circuit, cts); + } + } +} diff --git a/src/Components/test/testassets/ComponentsApp.Server/Startup.cs b/src/Components/test/testassets/ComponentsApp.Server/Startup.cs index aa5fcb6945..7c2f6e7f3c 100644 --- a/src/Components/test/testassets/ComponentsApp.Server/Startup.cs +++ b/src/Components/test/testassets/ComponentsApp.Server/Startup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +12,7 @@ namespace ComponentsApp.Server { public void ConfigureServices(IServiceCollection services) { + services.AddSingleton(); services.AddRazorComponents(); services.AddSingleton(); } diff --git a/src/Components/test/testassets/TestServer/Startup.cs b/src/Components/test/testassets/TestServer/Startup.cs index fb46b158ab..dfe4ab2215 100644 --- a/src/Components/test/testassets/TestServer/Startup.cs +++ b/src/Components/test/testassets/TestServer/Startup.cs @@ -46,7 +46,7 @@ namespace TestServer // we're not relying on any extra magic inside UseServerSideBlazor, since it's // important that people can set up these bits of middleware manually (e.g., to // swap in UseAzureSignalR instead of UseSignalR). - subdirApp.UseSignalR(route => route.MapHub(BlazorHub.DefaultPath)); + subdirApp.UseSignalR(route => route.MapHub(ComponentsHub.DefaultPath)); subdirApp.UseBlazor(); }); }