[Design]: Introduce CircuitHandler to handle circuit lifetime events
Partial fix to https://github.com/aspnet/AspNetCore/issues/6353
This commit is contained in:
parent
5a4a001d18
commit
e312d64194
|
|
@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
// add SignalR and BlazorHub automatically.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)>();
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <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>
|
||||
public class Circuit
|
||||
public sealed class Circuit
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current <see cref="Circuit"/>.
|
||||
/// </summary>
|
||||
public static Circuit Current => CircuitHost.Current?.Circuit;
|
||||
private readonly CircuitHost _circuitHost;
|
||||
|
||||
internal Circuit(CircuitHost circuitHost)
|
||||
{
|
||||
JSRuntime = circuitHost.JSRuntime;
|
||||
Services = circuitHost.Services;
|
||||
_circuitHost = circuitHost;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IJSRuntime"/> associated with this circuit.
|
||||
/// Gets the identifier for the <see cref="Circuit"/>.
|
||||
/// </summary>
|
||||
public IJSRuntime JSRuntime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IServiceProvider"/> associated with this circuit.
|
||||
/// </summary>
|
||||
public IServiceProvider Services { get; }
|
||||
public string Id => _circuitHost.CircuitId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CircuitHost> _current = new AsyncLocal<CircuitHost>();
|
||||
private readonly IServiceScope _scope;
|
||||
private readonly CircuitHandler[] _circuitHandlers;
|
||||
private bool _initialized;
|
||||
|
||||
private Action<IComponentsApplicationBuilder> _configure;
|
||||
|
||||
/// <summary>
|
||||
/// 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));
|
||||
|
||||
Microsoft.JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
|
||||
JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
|
||||
RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry);
|
||||
}
|
||||
|
||||
public event UnhandledExceptionEventHandler UnhandledException;
|
||||
|
||||
private bool _isInitialized;
|
||||
private Action<IComponentsApplicationBuilder> _configure;
|
||||
|
||||
public CircuitHost(
|
||||
IServiceScope scope,
|
||||
IClientProxy client,
|
||||
|
|
@ -53,9 +55,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
RemoteRenderer renderer,
|
||||
Action<IComponentsApplicationBuilder> 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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CircuitHandler>()
|
||||
.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<IJSRuntimeAccessor>() as DefaultJSRuntimeAccessor).JSRuntime = jsRuntime;
|
||||
|
|
|
|||
|
|
@ -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<DefaultCircuitFactoryOptions>(...)
|
||||
// callbacks to build up this dictionary mapping paths to startup actions
|
||||
public Dictionary<PathString, Action<IComponentsApplicationBuilder>> StartupActions { get; }
|
||||
|
||||
public DefaultCircuitFactoryOptions()
|
||||
{
|
||||
StartupActions = new Dictionary<PathString, Action<IComponentsApplicationBuilder>>();
|
||||
}
|
||||
internal Dictionary<PathString, Action<IComponentsApplicationBuilder>> StartupActions { get; }
|
||||
= new Dictionary<PathString, Action<IComponentsApplicationBuilder>>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ using Microsoft.Extensions.Logging;
|
|||
namespace Microsoft.AspNetCore.Components.Server
|
||||
{
|
||||
/// <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>
|
||||
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.
|
||||
/// </summary>
|
||||
public BlazorHub(
|
||||
ILogger<BlazorHub> logger,
|
||||
IServiceProvider services)
|
||||
public ComponentsHub(IServiceProvider services, ILogger<ComponentsHub> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_circuitFactory = services.GetRequiredService<CircuitFactory>();
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default endpoint path for incoming connections.
|
||||
/// </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];
|
||||
set => Context.Items[CircuitKey] = value;
|
||||
private set => Context.Items[CircuitKey] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended for framework use only. Applications should not call this method directly.
|
||||
/// </summary>
|
||||
public override Task OnDisconnectedAsync(Exception exception)
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
CircuitHost.Dispose();
|
||||
return base.OnDisconnectedAsync(exception);
|
||||
await CircuitHost.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -64,9 +64,9 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
var uriHelper = (RemoteUriHelper)circuitHost.Services.GetRequiredService<IUriHelper>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -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<DefaultCircuitFactoryOptions>(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<CircuitFactory, DefaultCircuitFactory>();
|
||||
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<IJSRuntime>(s => s.GetRequiredService<IJSRuntimeAccessor>().JSRuntime);
|
||||
services.TryAddScoped(s => s.GetRequiredService<IJSRuntimeAccessor>().JSRuntime);
|
||||
services.TryAddScoped<IUriHelper, RemoteUriHelper>();
|
||||
|
||||
// We've discussed with the SignalR team and believe it's OK to have repeated
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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<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 renderRegistry = new RendererRegistry();
|
||||
var jsRuntime = Mock.Of<IJSRuntime>();
|
||||
var syncContext = new CircuitSynchronizationContext();
|
||||
|
||||
var remoteRenderer = new TestRemoteRenderer(
|
||||
Mock.Of<IServiceProvider>(),
|
||||
renderRegistry,
|
||||
jsRuntime,
|
||||
remoteRenderer = remoteRenderer ?? GetRemoteRenderer();
|
||||
handlers = handlers ?? Array.Empty<CircuitHandler>();
|
||||
|
||||
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<IServiceProvider>(),
|
||||
new RendererRegistry(),
|
||||
Mock.Of<IJSRuntime>(),
|
||||
Mock.Of<IClientProxy>(),
|
||||
new CircuitSynchronizationContext());
|
||||
}
|
||||
|
||||
private class TestRemoteRenderer : RemoteRenderer
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
@ -8,4 +8,8 @@
|
|||
<Reference Include="Microsoft.AspNetCore.Components.Server" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CircuitHandler, LoggingCircuitHandler>();
|
||||
services.AddRazorComponents<App.Startup>();
|
||||
services.AddSingleton<WeatherForecastService, DefaultWeatherForecastService>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>(BlazorHub.DefaultPath));
|
||||
subdirApp.UseSignalR(route => route.MapHub<ComponentsHub>(ComponentsHub.DefaultPath));
|
||||
subdirApp.UseBlazor<BasicTestApp.Startup>();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue