[Design]: Introduce CircuitHandler to handle circuit lifetime events

Partial fix to https://github.com/aspnet/AspNetCore/issues/6353
This commit is contained in:
Pranav K 2019-01-23 16:45:56 -08:00
parent 5a4a001d18
commit e312d64194
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
17 changed files with 376 additions and 86 deletions

View File

@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Builder
// add SignalR and BlazorHub automatically. // add SignalR and BlazorHub automatically.
if (options.UseSignalRWithBlazorHub) if (options.UseSignalRWithBlazorHub)
{ {
builder.UseSignalR(route => route.MapHub<BlazorHub>(BlazorHub.DefaultPath)); builder.UseSignalR(route => route.MapHub<ComponentsHub>(ComponentsHub.DefaultPath));
} }
// Use embedded static content for /_framework // Use embedded static content for /_framework

View File

@ -7,9 +7,9 @@ using Microsoft.AspNetCore.Components.Builder;
namespace Microsoft.AspNetCore.Components.Hosting namespace Microsoft.AspNetCore.Components.Hosting
{ {
internal class ServerSideBlazorApplicationBuilder : IComponentsApplicationBuilder internal class ServerSideComponentsApplicationBuilder : IComponentsApplicationBuilder
{ {
public ServerSideBlazorApplicationBuilder(IServiceProvider services) public ServerSideComponentsApplicationBuilder(IServiceProvider services)
{ {
Services = services; Services = services;
Entries = new List<(Type componentType, string domElementSelector)>(); Entries = new List<(Type componentType, string domElementSelector)>();

View File

@ -1,35 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public class Circuit public sealed class Circuit
{ {
/// <summary> private readonly CircuitHost _circuitHost;
/// Gets the current <see cref="Circuit"/>.
/// </summary>
public static Circuit Current => CircuitHost.Current?.Circuit;
internal Circuit(CircuitHost circuitHost) internal Circuit(CircuitHost circuitHost)
{ {
JSRuntime = circuitHost.JSRuntime; _circuitHost = circuitHost;
Services = circuitHost.Services;
} }
/// <summary> /// <summary>
/// Gets the <see cref="IJSRuntime"/> associated with this circuit. /// Gets the identifier for the <see cref="Circuit"/>.
/// </summary> /// </summary>
public IJSRuntime JSRuntime { get; } public string Id => _circuitHost.CircuitId;
/// <summary>
/// Gets the <see cref="IServiceProvider"/> associated with this circuit.
/// </summary>
public IServiceProvider Services { get; }
} }
} }

View File

@ -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
{
/// <summary>
/// A <see cref="CircuitHandler"/> allows running code during specific lifetime events of a <see cref="Circuit"/>.
/// <list type="bullet">
/// <item>
/// <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/> is invoked after an initial circuit to the client
/// has been established.
/// </item>
/// <item>
/// <see cref="OnConnectionUpAsync(Circuit, CancellationToken)(Circuit, CancellationToken)"/> is invoked immediately after the completion of
/// <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/>. In addition, the method is invoked each time a connection is re-established
/// with a client after it's been dropped. <see cref="OnConnectionDownAsync(Circuit, CancellationToken)"/> is invoked each time a connection
/// is dropped.
/// </item>
/// <item>
/// <see cref="OnCircuitClosedAsync(Circuit, CancellationToken)"/> 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.
/// </item>
/// </list>
/// <ol>
/// </summary>
public abstract class CircuitHandler
{
/// <summary>
/// Gets the execution order for the current instance of <see cref="CircuitHandler"/>.
/// <para>
/// When multiple <see cref="CircuitHandler"/> instances are registered, the <see cref="Order"/>
/// property is used to determine the order in which instances are executed. When two handlers
/// have the same value for <see cref="Order"/>, their execution order is non-deterministic.
/// </para>
/// </summary>
/// <value>
/// Defaults to 0.
/// </value>
public virtual int Order => 0;
/// <summary>
/// Invoked when a new circuit was established.
/// </summary>
/// <param name="circuit">The <see cref="Circuit"/>.</param>
/// <param name="cts">The <see cref="CancellationToken"/>.</param>
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
public virtual Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask;
/// <summary>
/// Invoked when a connection to the client was established.
/// <para>
/// This method is executed once initially after <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/>
/// and once each for each reconnect during the lifetime of a circuit.
/// </para>
/// </summary>
/// <param name="circuit">The <see cref="Circuit"/>.</param>
/// <param name="cts">The <see cref="CancellationToken"/>.</param>
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
public virtual Task OnConnectionUpAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask;
/// <summary>
/// Invoked a connection to the client using was dropped.
/// </summary>
/// <param name="circuit">The <see cref="Circuit"/>.</param>
/// <param name="cts">The <see cref="CancellationToken"/>.</param>
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
public virtual Task OnConnectionDownAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask;
/// <summary>
/// Invoked when a new circuit is being discarded.
/// </summary>
/// <param name="circuit">The <see cref="Circuit"/>.</param>
/// <param name="cts">The <see cref="CancellationToken"/>.</param>
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
public virtual Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask;
}
}

View File

@ -14,9 +14,14 @@ using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Server.Circuits namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
internal class CircuitHost : IDisposable internal class CircuitHost : IAsyncDisposable
{ {
private static readonly AsyncLocal<CircuitHost> _current = new AsyncLocal<CircuitHost>(); private static readonly AsyncLocal<CircuitHost> _current = new AsyncLocal<CircuitHost>();
private readonly IServiceScope _scope;
private readonly CircuitHandler[] _circuitHandlers;
private bool _initialized;
private Action<IComponentsApplicationBuilder> _configure;
/// <summary> /// <summary>
/// Gets the current <see cref="Circuit"/>, if any. /// Gets the current <see cref="Circuit"/>, if any.
@ -37,15 +42,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
_current.Value = circuitHost ?? throw new ArgumentNullException(nameof(circuitHost)); _current.Value = circuitHost ?? throw new ArgumentNullException(nameof(circuitHost));
Microsoft.JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime); JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry); RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry);
} }
public event UnhandledExceptionEventHandler UnhandledException; public event UnhandledExceptionEventHandler UnhandledException;
private bool _isInitialized;
private Action<IComponentsApplicationBuilder> _configure;
public CircuitHost( public CircuitHost(
IServiceScope scope, IServiceScope scope,
IClientProxy client, IClientProxy client,
@ -53,9 +55,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
RemoteRenderer renderer, RemoteRenderer renderer,
Action<IComponentsApplicationBuilder> configure, Action<IComponentsApplicationBuilder> configure,
IJSRuntime jsRuntime, 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)); Client = client ?? throw new ArgumentNullException(nameof(client));
RendererRegistry = rendererRegistry ?? throw new ArgumentNullException(nameof(rendererRegistry)); RendererRegistry = rendererRegistry ?? throw new ArgumentNullException(nameof(rendererRegistry));
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
@ -66,11 +69,14 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Services = scope.ServiceProvider; Services = scope.ServiceProvider;
Circuit = new Circuit(this); Circuit = new Circuit(this);
_circuitHandlers = circuitHandlers;
Renderer.UnhandledException += Renderer_UnhandledException; Renderer.UnhandledException += Renderer_UnhandledException;
SynchronizationContext.UnhandledException += SynchronizationContext_UnhandledException; SynchronizationContext.UnhandledException += SynchronizationContext_UnhandledException;
} }
public string CircuitId { get; } = Guid.NewGuid().ToString();
public Circuit Circuit { get; } public Circuit Circuit { get; }
public IClientProxy Client { get; } public IClientProxy Client { get; }
@ -81,30 +87,40 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public RendererRegistry RendererRegistry { get; } public RendererRegistry RendererRegistry { get; }
public IServiceScope Scope { get; }
public IServiceProvider Services { get; } public IServiceProvider Services { get; }
public CircuitSynchronizationContext SynchronizationContext { 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); SetCurrentCircuitHost(this);
var builder = new ServerSideBlazorApplicationBuilder(Services); var builder = new ServerSideComponentsApplicationBuilder(Services);
_configure(builder); _configure(builder);
for (var i = 0; i < builder.Entries.Count; i++) for (var i = 0; i < builder.Entries.Count; i++)
{ {
var entry = builder.Entries[i]; var (componentType, domElementSelector) = builder.Entries[i];
Renderer.AddComponent(entry.componentType, entry.domElementSelector); 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) 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(); Renderer.Dispose();
} }
private void AssertInitialized() private void AssertInitialized()
{ {
if (!_isInitialized) if (!_initialized)
{ {
throw new InvalidOperationException("Something is calling into the circuit before Initialize() completes"); throw new InvalidOperationException("Something is calling into the circuit before Initialize() completes");
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Linq;
using Microsoft.AspNetCore.Components.Browser; using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering; using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -33,7 +34,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
if (!_options.StartupActions.TryGetValue(httpContext.Request.Path, out var config)) 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); throw new InvalidOperationException(message);
} }
@ -43,6 +44,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var synchronizationContext = new CircuitSynchronizationContext(); var synchronizationContext = new CircuitSynchronizationContext();
var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client, synchronizationContext); var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client, synchronizationContext);
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
.OrderBy(h => h.Order)
.ToArray();
var circuitHost = new CircuitHost( var circuitHost = new CircuitHost(
scope, scope,
client, client,
@ -50,7 +55,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
renderer, renderer,
config, config,
jsRuntime, jsRuntime,
synchronizationContext); synchronizationContext,
circuitHandlers);
// Initialize per-circuit data that services need // Initialize per-circuit data that services need
(circuitHost.Services.GetRequiredService<IJSRuntimeAccessor>() as DefaultJSRuntimeAccessor).JSRuntime = jsRuntime; (circuitHost.Services.GetRequiredService<IJSRuntimeAccessor>() as DefaultJSRuntimeAccessor).JSRuntime = jsRuntime;

View File

@ -6,17 +6,13 @@ using System.Collections.Generic;
using Microsoft.AspNetCore.Components.Builder; using Microsoft.AspNetCore.Components.Builder;
using Microsoft.AspNetCore.Http; 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<DefaultCircuitFactoryOptions>(...) // During the DI configuration phase, we use Configure<DefaultCircuitFactoryOptions>(...)
// callbacks to build up this dictionary mapping paths to startup actions // callbacks to build up this dictionary mapping paths to startup actions
public Dictionary<PathString, Action<IComponentsApplicationBuilder>> StartupActions { get; } internal Dictionary<PathString, Action<IComponentsApplicationBuilder>> StartupActions { get; }
= new Dictionary<PathString, Action<IComponentsApplicationBuilder>>();
public DefaultCircuitFactoryOptions()
{
StartupActions = new Dictionary<PathString, Action<IComponentsApplicationBuilder>>();
}
} }
} }

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
[JSInvokable(nameof(NotifyLocationChanged))] [JSInvokable(nameof(NotifyLocationChanged))]
public static void NotifyLocationChanged(string uriAbsolute) public static void NotifyLocationChanged(string uriAbsolute)
{ {
var circuit = Circuit.Current; var circuit = CircuitHost.Current;
if (circuit == null) if (circuit == null)
{ {
var message = $"{nameof(NotifyLocationChanged)} called without a circuit."; var message = $"{nameof(NotifyLocationChanged)} called without a circuit.";

View File

@ -13,9 +13,9 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Server namespace Microsoft.AspNetCore.Components.Server
{ {
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public sealed class BlazorHub : Hub public sealed class ComponentsHub : Hub
{ {
private static readonly object CircuitKey = new object(); private static readonly object CircuitKey = new object();
private readonly CircuitFactory _circuitFactory; private readonly CircuitFactory _circuitFactory;
@ -25,32 +25,32 @@ namespace Microsoft.AspNetCore.Components.Server
/// Intended for framework use only. Applications should not instantiate /// Intended for framework use only. Applications should not instantiate
/// this class directly. /// this class directly.
/// </summary> /// </summary>
public BlazorHub( public ComponentsHub(IServiceProvider services, ILogger<ComponentsHub> logger)
ILogger<BlazorHub> logger,
IServiceProvider services)
{ {
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_circuitFactory = services.GetRequiredService<CircuitFactory>(); _circuitFactory = services.GetRequiredService<CircuitFactory>();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
/// <summary> /// <summary>
/// Gets the default endpoint path for incoming connections. /// Gets the default endpoint path for incoming connections.
/// </summary> /// </summary>
public static PathString DefaultPath => "/_blazor"; public static PathString DefaultPath { get; } = "/_blazor";
private CircuitHost CircuitHost /// <summary>
/// For unit testing only.
/// </summary>
internal CircuitHost CircuitHost
{ {
get => (CircuitHost)Context.Items[CircuitKey]; get => (CircuitHost)Context.Items[CircuitKey];
set => Context.Items[CircuitKey] = value; private set => Context.Items[CircuitKey] = value;
} }
/// <summary> /// <summary>
/// Intended for framework use only. Applications should not call this method directly. /// Intended for framework use only. Applications should not call this method directly.
/// </summary> /// </summary>
public override Task OnDisconnectedAsync(Exception exception) public override async Task OnDisconnectedAsync(Exception exception)
{ {
CircuitHost.Dispose(); await CircuitHost.DisposeAsync();
return base.OnDisconnectedAsync(exception);
} }
/// <summary> /// <summary>
@ -64,9 +64,9 @@ namespace Microsoft.AspNetCore.Components.Server
var uriHelper = (RemoteUriHelper)circuitHost.Services.GetRequiredService<IUriHelper>(); var uriHelper = (RemoteUriHelper)circuitHost.Services.GetRequiredService<IUriHelper>();
uriHelper.Initialize(uriAbsolute, baseUriAbsolute); uriHelper.Initialize(uriAbsolute, baseUriAbsolute);
// If initialization fails, this will throw. The caller will explode if they // If initialization fails, this will throw. The caller will fail if they try to call into any interop API.
// try to call into any interop API. await circuitHost.InitializeAsync(Context.ConnectionAborted);
await circuitHost.InitializeAsync();
CircuitHost = circuitHost; CircuitHost = circuitHost;
} }

View File

@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Components.Services; using Microsoft.AspNetCore.Components.Services;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.JSInterop;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
{ {
@ -74,7 +73,7 @@ namespace Microsoft.Extensions.DependencyInjection
// TStartup's Configure method". // TStartup's Configure method".
services.Configure<DefaultCircuitFactoryOptions>(circuitFactoryOptions => services.Configure<DefaultCircuitFactoryOptions>(circuitFactoryOptions =>
{ {
var endpoint = BlazorHub.DefaultPath; // TODO: allow configuring this var endpoint = ComponentsHub.DefaultPath; // TODO: allow configuring this
if (circuitFactoryOptions.StartupActions.ContainsKey(endpoint)) if (circuitFactoryOptions.StartupActions.ContainsKey(endpoint))
{ {
throw new InvalidOperationException( 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. // Components entrypoints, this lot is the same and repeated registrations are a no-op.
services.TryAddSingleton<CircuitFactory, DefaultCircuitFactory>(); services.TryAddSingleton<CircuitFactory, DefaultCircuitFactory>();
services.TryAddScoped<ICircuitAccessor, DefaultCircuitAccessor>(); services.TryAddScoped<ICircuitAccessor, DefaultCircuitAccessor>();
services.TryAddScoped<Circuit>(s => s.GetRequiredService<ICircuitAccessor>().Circuit); services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
services.TryAddScoped<IJSRuntimeAccessor, DefaultJSRuntimeAccessor>(); services.TryAddScoped<IJSRuntimeAccessor, DefaultJSRuntimeAccessor>();
services.TryAddScoped<IJSRuntime>(s => s.GetRequiredService<IJSRuntimeAccessor>().JSRuntime); services.TryAddScoped(s => s.GetRequiredService<IJSRuntimeAccessor>().JSRuntime);
services.TryAddScoped<IUriHelper, RemoteUriHelper>(); services.TryAddScoped<IUriHelper, RemoteUriHelper>();
// We've discussed with the SignalR team and believe it's OK to have repeated // We've discussed with the SignalR team and believe it's OK to have repeated

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>

View File

@ -2,3 +2,5 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Cli, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Cli, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Browser; using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering; using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
@ -16,30 +17,143 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public class CircuitHostTest public class CircuitHostTest
{ {
[Fact] [Fact]
public void Dispose_DisposesResources() public async Task DisposeAsync_DisposesResources()
{ {
// Arrange // Arrange
var serviceScope = new Mock<IServiceScope>(); var serviceScope = new Mock<IServiceScope>();
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<CircuitHandler>(MockBehavior.Strict);
var handler2 = new Mock<CircuitHandler>(MockBehavior.Strict);
var sequence = new MockSequence();
handler1
.InSequence(sequence)
.Setup(h => h.OnCircuitOpenedAsync(It.IsAny<Circuit>(), cancellationToken))
.Returns(Task.CompletedTask)
.Verifiable();
handler2
.InSequence(sequence)
.Setup(h => h.OnCircuitOpenedAsync(It.IsAny<Circuit>(), cancellationToken))
.Returns(Task.CompletedTask)
.Verifiable();
handler1
.InSequence(sequence)
.Setup(h => h.OnConnectionUpAsync(It.IsAny<Circuit>(), cancellationToken))
.Returns(Task.CompletedTask)
.Verifiable();
handler2
.InSequence(sequence)
.Setup(h => h.OnConnectionUpAsync(It.IsAny<Circuit>(), 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<CircuitHandler>(MockBehavior.Strict);
var handler2 = new Mock<CircuitHandler>(MockBehavior.Strict);
var sequence = new MockSequence();
handler1
.InSequence(sequence)
.Setup(h => h.OnConnectionDownAsync(It.IsAny<Circuit>(), cancellationToken))
.Returns(Task.CompletedTask)
.Verifiable();
handler2
.InSequence(sequence)
.Setup(h => h.OnConnectionDownAsync(It.IsAny<Circuit>(), cancellationToken))
.Returns(Task.CompletedTask)
.Verifiable();
handler1
.InSequence(sequence)
.Setup(h => h.OnCircuitClosedAsync(It.IsAny<Circuit>(), cancellationToken))
.Returns(Task.CompletedTask)
.Verifiable();
handler2
.InSequence(sequence)
.Setup(h => h.OnCircuitClosedAsync(It.IsAny<Circuit>(), 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<IServiceScope>();
var clientProxy = Mock.Of<IClientProxy>(); var clientProxy = Mock.Of<IClientProxy>();
var renderRegistry = new RendererRegistry(); var renderRegistry = new RendererRegistry();
var jsRuntime = Mock.Of<IJSRuntime>(); var jsRuntime = Mock.Of<IJSRuntime>();
var syncContext = new CircuitSynchronizationContext(); var syncContext = new CircuitSynchronizationContext();
var remoteRenderer = new TestRemoteRenderer( remoteRenderer = remoteRenderer ?? GetRemoteRenderer();
Mock.Of<IServiceProvider>(), handlers = handlers ?? Array.Empty<CircuitHandler>();
renderRegistry,
jsRuntime, return new CircuitHost(
serviceScope,
clientProxy, clientProxy,
syncContext); renderRegistry,
remoteRenderer,
configure: _ => { },
jsRuntime: jsRuntime,
synchronizationContext:
syncContext,
handlers);
}
var circuitHost = new CircuitHost(serviceScope.Object, clientProxy, renderRegistry, remoteRenderer, configure: _ => { }, jsRuntime: jsRuntime, synchronizationContext: syncContext); private static TestRemoteRenderer GetRemoteRenderer()
{
// Act return new TestRemoteRenderer(
circuitHost.Dispose(); Mock.Of<IServiceProvider>(),
new RendererRegistry(),
// Assert Mock.Of<IJSRuntime>(),
serviceScope.Verify(s => s.Dispose(), Times.Once()); Mock.Of<IClientProxy>(),
Assert.True(remoteRenderer.Disposed); new CircuitSynchronizationContext());
} }
private class TestRemoteRenderer : RemoteRenderer private class TestRemoteRenderer : RemoteRenderer

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework> <TargetFramework>netcoreapp3.0</TargetFramework>
@ -8,4 +8,8 @@
<Reference Include="Microsoft.AspNetCore.Components.Server" /> <Reference Include="Microsoft.AspNetCore.Components.Server" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Logging.Testing" />
</ItemGroup>
</Project> </Project>

View File

@ -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<LoggingCircuitHandler> _logger;
private static Action<ILogger, string, Exception> _circuitOpened;
private static Action<ILogger, string, Exception> _connectionUp;
private static Action<ILogger, string, Exception> _connectionDown;
private static Action<ILogger, string, Exception> _circuitClosed;
public LoggingCircuitHandler(ILogger<LoggingCircuitHandler> logger)
{
_logger = logger;
_circuitOpened = LoggerMessage.Define<string>(
logLevel: LogLevel.Information,
1,
formatString: "Circuit opened for {circuitId}.");
_connectionUp = LoggerMessage.Define<string>(
logLevel: LogLevel.Information,
2,
formatString: "Connection up for {circuitId}.");
_connectionDown = LoggerMessage.Define<string>(
logLevel: LogLevel.Information,
3,
formatString: "Connection down for {circuitId}.");
_circuitClosed = LoggerMessage.Define<string>(
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);
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // 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.Builder;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -11,6 +12,7 @@ namespace ComponentsApp.Server
{ {
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.AddSingleton<CircuitHandler, LoggingCircuitHandler>();
services.AddRazorComponents<App.Startup>(); services.AddRazorComponents<App.Startup>();
services.AddSingleton<WeatherForecastService, DefaultWeatherForecastService>(); services.AddSingleton<WeatherForecastService, DefaultWeatherForecastService>();
} }

View File

@ -46,7 +46,7 @@ namespace TestServer
// we're not relying on any extra magic inside UseServerSideBlazor, since it's // 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 // important that people can set up these bits of middleware manually (e.g., to
// swap in UseAzureSignalR instead of UseSignalR). // swap in UseAzureSignalR instead of UseSignalR).
subdirApp.UseSignalR(route => route.MapHub<BlazorHub>(BlazorHub.DefaultPath)); subdirApp.UseSignalR(route => route.MapHub<ComponentsHub>(ComponentsHub.DefaultPath));
subdirApp.UseBlazor<BasicTestApp.Startup>(); subdirApp.UseBlazor<BasicTestApp.Startup>();
}); });
} }