[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.
|
// 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
|
||||||
|
|
|
||||||
|
|
@ -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)>();
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.";
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
|
||||||
|
|
@ -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")]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// 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>();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue