[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.
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

View File

@ -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)>();

View File

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

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
{
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");
}

View File

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

View File

@ -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>>();
}
}

View File

@ -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.";

View File

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

View File

@ -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

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@ -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

View File

@ -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>

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.
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>();
}

View File

@ -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>();
});
}