Skip to content

Commit d11b9c1

Browse files
committed
[Design]: Introduce CircuitHandler to handle circuit lifetime events
Partial fix to #6353
1 parent ace3387 commit d11b9c1

15 files changed

+298
-91
lines changed

src/Components/Server/src/Builder/RazorComponentsApplicationBuilderExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static IApplicationBuilder UseRazorComponents<TStartup>(
5050
// add SignalR and BlazorHub automatically.
5151
if (options.UseSignalRWithBlazorHub)
5252
{
53-
builder.UseSignalR(route => route.MapHub<BlazorHub>(BlazorHub.DefaultPath));
53+
builder.UseSignalR(route => route.MapHub<ComponentsHub>(ComponentsHub.DefaultPath));
5454
}
5555

5656
// Use embedded static content for /_framework

src/Components/Server/src/Builder/ServerSideBlazorApplicationBuilder.cs renamed to src/Components/Server/src/Builder/ServerSideComponentsApplicationBuilder.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
namespace Microsoft.AspNetCore.Components.Hosting
99
{
10-
internal class ServerSideBlazorApplicationBuilder : IComponentsApplicationBuilder
10+
internal class ServerSideComponentsApplicationBuilder : IComponentsApplicationBuilder
1111
{
12-
public ServerSideBlazorApplicationBuilder(IServiceProvider services)
12+
public ServerSideComponentsApplicationBuilder(IServiceProvider services)
1313
{
1414
Services = services;
1515
Entries = new List<(Type componentType, string domElementSelector)>();
+5-19
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,21 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using System;
5-
using Microsoft.JSInterop;
6-
74
namespace Microsoft.AspNetCore.Components.Server.Circuits
85
{
96
/// <summary>
10-
/// Represents an active connection between a Blazor server and a client.
7+
/// Represents an active connection between an ASP.NET Core Component on the server and a client.
118
/// </summary>
129
public class Circuit
1310
{
14-
/// <summary>
15-
/// Gets the current <see cref="Circuit"/>.
16-
/// </summary>
17-
public static Circuit Current => CircuitHost.Current?.Circuit;
18-
19-
internal Circuit(CircuitHost circuitHost)
11+
internal Circuit(string id)
2012
{
21-
JSRuntime = circuitHost.JSRuntime;
22-
Services = circuitHost.Services;
13+
Id = id;
2314
}
2415

2516
/// <summary>
26-
/// Gets the <see cref="IJSRuntime"/> associated with this circuit.
27-
/// </summary>
28-
public IJSRuntime JSRuntime { get; }
29-
30-
/// <summary>
31-
/// Gets the <see cref="IServiceProvider"/> associated with this circuit.
17+
/// Gets the identifier for the <see cref="Circuit"/>.
3218
/// </summary>
33-
public IServiceProvider Services { get; }
19+
public string Id { get; }
3420
}
3521
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNetCore.Http;
7+
8+
namespace Microsoft.AspNetCore.Components.Server.Circuits
9+
{
10+
/// <summary>
11+
/// A <see cref="CircuitHandler"/> allows running code during specific lifetime events of a <see cref="Circuit"/>.
12+
/// <list type="bullet">
13+
/// <item>
14+
/// <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/> is invoked after an initial connection to the client
15+
/// has been established.
16+
/// </item>
17+
/// <item>
18+
/// <see cref="OnConnectionUpAsync(Circuit, CancellationToken)(Circuit, CancellationToken)"/> is invoked immediately after the completion of
19+
/// <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/>. In addition, the method is invoked each time a connection is re-established
20+
/// with a client after it's been dropped. <see cref="OnConnectionDownAsync(Circuit, CancellationToken)"/> is invoked each time a connection
21+
/// is dropped.
22+
/// </item>
23+
/// <item>
24+
/// <see cref="OnCircuitClosedAsync(Circuit, CancellationToken)"/> is invoked prior to the server evicting the connection to the client.
25+
/// Application users may use this event to save state for a client that can be later rehydrated.
26+
/// </item>
27+
/// </list>
28+
/// <ol>
29+
/// </summary>
30+
public abstract class CircuitHandler
31+
{
32+
internal static readonly CircuitHandler NullHandler = new NullCircuitHandler();
33+
34+
/// <summary>
35+
/// Invoked when a new circuit was established.
36+
/// </summary>
37+
/// <param name="circuit">The <see cref="Circuit"/>.</param>
38+
/// <param name="cts">The <see cref="CancellationToken"/>.</param>
39+
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
40+
public virtual Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask;
41+
42+
/// <summary>
43+
/// Invoked when a connection to the client was established.
44+
/// <para>
45+
/// This method is executed once initially after <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/>
46+
/// and once each for each reconnect during the lifetime of a circuit.
47+
/// </para>
48+
/// </summary>
49+
/// <param name="circuit">The <see cref="Circuit"/>.</param>
50+
/// <param name="cts">The <see cref="CancellationToken"/>.</param>
51+
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
52+
public virtual Task OnConnectionUpAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask;
53+
54+
/// <summary>
55+
/// Invoked a connection to the client using was dropped.
56+
/// </summary>
57+
/// <param name="circuit">The <see cref="Circuit"/>.</param>
58+
/// <param name="cts">The <see cref="CancellationToken"/>.</param>
59+
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
60+
public virtual Task OnConnectionDownAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask;
61+
62+
63+
/// <summary>
64+
/// Invoked when a new circuit is being discarded.
65+
/// </summary>
66+
/// <param name="circuit">The <see cref="Circuit"/>.</param>
67+
/// <param name="cts">The <see cref="CancellationToken"/>.</param>
68+
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
69+
public virtual Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cts) => Task.CompletedTask;
70+
71+
private class NullCircuitHandler : CircuitHandler
72+
{
73+
74+
}
75+
}
76+
}

src/Components/Server/src/Circuits/CircuitHost.cs

+29-15
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
namespace Microsoft.AspNetCore.Components.Server.Circuits
1616
{
17-
internal class CircuitHost : IDisposable
17+
internal class CircuitHost : IAsyncDisposable
1818
{
1919
private static readonly AsyncLocal<CircuitHost> _current = new AsyncLocal<CircuitHost>();
20+
private readonly IServiceScope _scope;
21+
private Action<IComponentsApplicationBuilder> _configure;
22+
private bool _isInitialized;
2023

2124
/// <summary>
2225
/// Gets the current <see cref="Circuit"/>, if any.
@@ -37,25 +40,24 @@ public static void SetCurrentCircuitHost(CircuitHost circuitHost)
3740
{
3841
_current.Value = circuitHost ?? throw new ArgumentNullException(nameof(circuitHost));
3942

40-
Microsoft.JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
43+
JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
4144
RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry);
4245
}
4346

4447
public event UnhandledExceptionEventHandler UnhandledException;
4548

46-
private bool _isInitialized;
47-
private Action<IComponentsApplicationBuilder> _configure;
48-
4949
public CircuitHost(
5050
IServiceScope scope,
5151
IClientProxy client,
5252
RendererRegistry rendererRegistry,
5353
RemoteRenderer renderer,
5454
Action<IComponentsApplicationBuilder> configure,
5555
IJSRuntime jsRuntime,
56-
CircuitSynchronizationContext synchronizationContext)
56+
CircuitSynchronizationContext synchronizationContext,
57+
Circuit circuit,
58+
CircuitHandler circuitHandler)
5759
{
58-
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
60+
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
5961
Client = client ?? throw new ArgumentNullException(nameof(client));
6062
RendererRegistry = rendererRegistry ?? throw new ArgumentNullException(nameof(rendererRegistry));
6163
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
@@ -65,7 +67,8 @@ public CircuitHost(
6567

6668
Services = scope.ServiceProvider;
6769

68-
Circuit = new Circuit(this);
70+
Circuit = circuit;
71+
CircuitHandler = circuitHandler;
6972

7073
Renderer.UnhandledException += Renderer_UnhandledException;
7174
SynchronizationContext.UnhandledException += SynchronizationContext_UnhandledException;
@@ -81,19 +84,21 @@ public CircuitHost(
8184

8285
public RendererRegistry RendererRegistry { get; }
8386

84-
public IServiceScope Scope { get; }
85-
8687
public IServiceProvider Services { get; }
8788

8889
public CircuitSynchronizationContext SynchronizationContext { get; }
8990

90-
public async Task InitializeAsync()
91+
public CircuitHandler CircuitHandler { get; }
92+
93+
public CancellationToken ConnectionAborted { get; }
94+
95+
public async Task InitializeAsync(CancellationToken cancellationToken)
9196
{
92-
await SynchronizationContext.Invoke(() =>
97+
await SynchronizationContext.InvokeAsync(async () =>
9398
{
9499
SetCurrentCircuitHost(this);
95100

96-
var builder = new ServerSideBlazorApplicationBuilder(Services);
101+
var builder = new ServerSideComponentsApplicationBuilder(Services);
97102

98103
_configure(builder);
99104

@@ -102,6 +107,9 @@ await SynchronizationContext.Invoke(() =>
102107
var entry = builder.Entries[i];
103108
Renderer.AddComponent(entry.componentType, entry.domElementSelector);
104109
}
110+
111+
await CircuitHandler.OnCircuitOpenedAsync(Circuit, cancellationToken);
112+
await CircuitHandler.OnConnectionUpAsync(Circuit, cancellationToken);
105113
});
106114

107115
_isInitialized = true;
@@ -126,9 +134,15 @@ await SynchronizationContext.Invoke(() =>
126134
}
127135
}
128136

129-
public void Dispose()
137+
public async ValueTask DisposeAsync()
130138
{
131-
Scope.Dispose();
139+
await SynchronizationContext.InvokeAsync(async () =>
140+
{
141+
await CircuitHandler.OnConnectionDownAsync(Circuit, default);
142+
await CircuitHandler.OnCircuitClosedAsync(Circuit, default);
143+
});
144+
145+
_scope.Dispose();
132146
Renderer.Dispose();
133147
}
134148

src/Components/Server/src/Circuits/DefaultCircuitFactory.cs

+27-4
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
1414
internal class DefaultCircuitFactory : CircuitFactory
1515
{
1616
private readonly IServiceScopeFactory _scopeFactory;
17-
private readonly DefaultCircuitFactoryOptions _options;
17+
private readonly ComponentsServerOptions _options;
1818

1919
public DefaultCircuitFactory(
2020
IServiceScopeFactory scopeFactory,
21-
IOptions<DefaultCircuitFactoryOptions> options)
21+
IOptions<ComponentsServerOptions> options)
2222
{
2323
if (options == null)
2424
{
@@ -33,30 +33,53 @@ public override CircuitHost CreateCircuitHost(HttpContext httpContext, IClientPr
3333
{
3434
if (!_options.StartupActions.TryGetValue(httpContext.Request.Path, out var config))
3535
{
36-
var message = $"Could not find a Blazor startup action for request path {httpContext.Request.Path}";
36+
var message = $"Could not find an ASP.NET Core Components startup action for request path '{httpContext.Request.Path}'.";
3737
throw new InvalidOperationException(message);
3838
}
3939

40+
var circuit = new Circuit(Guid.NewGuid().ToString());
4041
var scope = _scopeFactory.CreateScope();
4142
var jsRuntime = new RemoteJSRuntime(client);
4243
var rendererRegistry = new RendererRegistry();
4344
var synchronizationContext = new CircuitSynchronizationContext();
4445
var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client, synchronizationContext);
4546

47+
var circuitHandler = GetCircuitHandler(httpContext, scope);
48+
4649
var circuitHost = new CircuitHost(
4750
scope,
4851
client,
4952
rendererRegistry,
5053
renderer,
5154
config,
5255
jsRuntime,
53-
synchronizationContext);
56+
synchronizationContext,
57+
circuit,
58+
circuitHandler);
5459

5560
// Initialize per-circuit data that services need
5661
(circuitHost.Services.GetRequiredService<IJSRuntimeAccessor>() as DefaultJSRuntimeAccessor).JSRuntime = jsRuntime;
5762
(circuitHost.Services.GetRequiredService<ICircuitAccessor>() as DefaultCircuitAccessor).Circuit = circuitHost.Circuit;
5863

5964
return circuitHost;
6065
}
66+
67+
private CircuitHandler GetCircuitHandler(HttpContext httpContext, IServiceScope scope)
68+
{
69+
// Is there a specific handler for this component hub?
70+
if (!_options.CircuitHandlers.TryGetValue(httpContext.Request.Path, out var handlerType))
71+
{
72+
// Nope, perhaps there's a default one specified.
73+
handlerType = _options.DefaultCircuitHandler;
74+
}
75+
76+
var circuitHandler = CircuitHandler.NullHandler;
77+
if (handlerType != null)
78+
{
79+
circuitHandler = (CircuitHandler)scope.ServiceProvider.GetRequiredService(handlerType);
80+
}
81+
82+
return circuitHandler;
83+
}
6184
}
6285
}

src/Components/Server/src/Circuits/DefaultCircuitFactoryOptions.cs

-22
This file was deleted.

src/Components/Server/src/Circuits/RemoteUriHelper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public void Initialize(string uriAbsolute, string baseUriAbsolute)
4848
[JSInvokable(nameof(NotifyLocationChanged))]
4949
public static void NotifyLocationChanged(string uriAbsolute)
5050
{
51-
var circuit = Circuit.Current;
51+
var circuit = CircuitHost.Current;
5252
if (circuit == null)
5353
{
5454
var message = $"{nameof(NotifyLocationChanged)} called without a circuit.";

0 commit comments

Comments
 (0)