Skip to content

Commit 2e54d64

Browse files
authored
Introduce CircuitHandler to handle circuit lifetime events (#6971)
Introduce CircuitHandler to handle circuit lifetime events Partial fix to #6353
2 parents e2c67ba + 62d10bc commit 2e54d64

17 files changed

+369
-84
lines changed

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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)>();
Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,23 @@
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 a link between a ASP.NET Core Component on the server and a client.
118
/// </summary>
12-
public class Circuit
9+
public sealed class Circuit
1310
{
14-
/// <summary>
15-
/// Gets the current <see cref="Circuit"/>.
16-
/// </summary>
17-
public static Circuit Current => CircuitHost.Current?.Circuit;
11+
private readonly CircuitHost _circuitHost;
1812

1913
internal Circuit(CircuitHost circuitHost)
2014
{
21-
JSRuntime = circuitHost.JSRuntime;
22-
Services = circuitHost.Services;
15+
_circuitHost = circuitHost;
2316
}
2417

2518
/// <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.
19+
/// Gets the identifier for the <see cref="Circuit"/>.
3220
/// </summary>
33-
public IServiceProvider Services { get; }
21+
public string Id => _circuitHost.CircuitId;
3422
}
3523
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
7+
namespace Microsoft.AspNetCore.Components.Server.Circuits
8+
{
9+
/// <summary>
10+
/// A <see cref="CircuitHandler"/> allows running code during specific lifetime events of a <see cref="Circuit"/>.
11+
/// <list type="bullet">
12+
/// <item>
13+
/// <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/> is invoked after an initial circuit to the client
14+
/// has been established.
15+
/// </item>
16+
/// <item>
17+
/// <see cref="OnConnectionUpAsync(Circuit, CancellationToken)(Circuit, CancellationToken)"/> is invoked immediately after the completion of
18+
/// <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/>. In addition, the method is invoked each time a connection is re-established
19+
/// with a client after it's been dropped. <see cref="OnConnectionDownAsync(Circuit, CancellationToken)"/> is invoked each time a connection
20+
/// is dropped.
21+
/// </item>
22+
/// <item>
23+
/// <see cref="OnCircuitClosedAsync(Circuit, CancellationToken)"/> is invoked prior to the server evicting the circuit to the client.
24+
/// Application users may use this event to save state for a client that can be later rehydrated.
25+
/// </item>
26+
/// </list>
27+
/// <ol>
28+
/// </summary>
29+
public abstract class CircuitHandler
30+
{
31+
/// <summary>
32+
/// Gets the execution order for the current instance of <see cref="CircuitHandler"/>.
33+
/// <para>
34+
/// When multiple <see cref="CircuitHandler"/> instances are registered, the <see cref="Order"/>
35+
/// property is used to determine the order in which instances are executed. When two handlers
36+
/// have the same value for <see cref="Order"/>, their execution order is non-deterministic.
37+
/// </para>
38+
/// </summary>
39+
/// <value>
40+
/// Defaults to 0.
41+
/// </value>
42+
public virtual int Order => 0;
43+
44+
/// <summary>
45+
/// Invoked when a new circuit was established.
46+
/// </summary>
47+
/// <param name="circuit">The <see cref="Circuit"/>.</param>
48+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that notifies when the client connection is aborted.</param>
49+
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
50+
public virtual Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken) => Task.CompletedTask;
51+
52+
/// <summary>
53+
/// Invoked when a connection to the client was established.
54+
/// <para>
55+
/// This method is executed once initially after <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/>
56+
/// and once each for each reconnect during the lifetime of a circuit.
57+
/// </para>
58+
/// </summary>
59+
/// <param name="circuit">The <see cref="Circuit"/>.</param>
60+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that notifies when the client connection is aborted.</param>
61+
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
62+
public virtual Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken) => Task.CompletedTask;
63+
64+
/// <summary>
65+
/// Invoked when a connection to the client was dropped.
66+
/// </summary>
67+
/// <param name="circuit">The <see cref="Circuit"/>.</param>
68+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
69+
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
70+
public virtual Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken) => Task.CompletedTask;
71+
72+
73+
/// <summary>
74+
/// Invoked when a new circuit is being discarded.
75+
/// </summary>
76+
/// <param name="circuit">The <see cref="Circuit"/>.</param>
77+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
78+
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
79+
public virtual Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken) => Task.CompletedTask;
80+
}
81+
}

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

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
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 readonly CircuitHandler[] _circuitHandlers;
22+
private bool _initialized;
23+
24+
private Action<IComponentsApplicationBuilder> _configure;
2025

2126
/// <summary>
2227
/// Gets the current <see cref="Circuit"/>, if any.
@@ -37,25 +42,23 @@ public static void SetCurrentCircuitHost(CircuitHost circuitHost)
3742
{
3843
_current.Value = circuitHost ?? throw new ArgumentNullException(nameof(circuitHost));
3944

40-
Microsoft.JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
45+
JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
4146
RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry);
4247
}
4348

4449
public event UnhandledExceptionEventHandler UnhandledException;
4550

46-
private bool _isInitialized;
47-
private Action<IComponentsApplicationBuilder> _configure;
48-
4951
public CircuitHost(
5052
IServiceScope scope,
5153
IClientProxy client,
5254
RendererRegistry rendererRegistry,
5355
RemoteRenderer renderer,
5456
Action<IComponentsApplicationBuilder> configure,
5557
IJSRuntime jsRuntime,
56-
CircuitSynchronizationContext synchronizationContext)
58+
CircuitSynchronizationContext synchronizationContext,
59+
CircuitHandler[] circuitHandlers)
5760
{
58-
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
61+
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
5962
Client = client ?? throw new ArgumentNullException(nameof(client));
6063
RendererRegistry = rendererRegistry ?? throw new ArgumentNullException(nameof(rendererRegistry));
6164
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
@@ -66,11 +69,14 @@ public CircuitHost(
6669
Services = scope.ServiceProvider;
6770

6871
Circuit = new Circuit(this);
72+
_circuitHandlers = circuitHandlers;
6973

7074
Renderer.UnhandledException += Renderer_UnhandledException;
7175
SynchronizationContext.UnhandledException += SynchronizationContext_UnhandledException;
7276
}
7377

78+
public string CircuitId { get; } = Guid.NewGuid().ToString();
79+
7480
public Circuit Circuit { get; }
7581

7682
public IClientProxy Client { get; }
@@ -81,30 +87,38 @@ public CircuitHost(
8187

8288
public RendererRegistry RendererRegistry { get; }
8389

84-
public IServiceScope Scope { get; }
85-
8690
public IServiceProvider Services { get; }
8791

8892
public CircuitSynchronizationContext SynchronizationContext { get; }
8993

90-
public async Task InitializeAsync()
94+
public async Task InitializeAsync(CancellationToken cancellationToken)
9195
{
92-
await SynchronizationContext.Invoke(() =>
96+
await SynchronizationContext.InvokeAsync(async () =>
9397
{
9498
SetCurrentCircuitHost(this);
9599

96-
var builder = new ServerSideBlazorApplicationBuilder(Services);
100+
var builder = new ServerSideComponentsApplicationBuilder(Services);
97101

98102
_configure(builder);
99103

100104
for (var i = 0; i < builder.Entries.Count; i++)
101105
{
102-
var entry = builder.Entries[i];
103-
Renderer.AddComponent(entry.componentType, entry.domElementSelector);
106+
var (componentType, domElementSelector) = builder.Entries[i];
107+
Renderer.AddComponent(componentType, domElementSelector);
108+
}
109+
110+
for (var i = 0; i < _circuitHandlers.Length; i++)
111+
{
112+
await _circuitHandlers[i].OnCircuitOpenedAsync(Circuit, cancellationToken);
113+
}
114+
115+
for (var i = 0; i < _circuitHandlers.Length; i++)
116+
{
117+
await _circuitHandlers[i].OnConnectionUpAsync(Circuit, cancellationToken);
104118
}
105119
});
106120

107-
_isInitialized = true;
121+
_initialized = true;
108122
}
109123

110124
public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
@@ -126,15 +140,28 @@ await SynchronizationContext.Invoke(() =>
126140
}
127141
}
128142

129-
public void Dispose()
143+
public async ValueTask DisposeAsync()
130144
{
131-
Scope.Dispose();
145+
await SynchronizationContext.InvokeAsync(async () =>
146+
{
147+
for (var i = 0; i < _circuitHandlers.Length; i++)
148+
{
149+
await _circuitHandlers[i].OnConnectionDownAsync(Circuit, default);
150+
}
151+
152+
for (var i = 0; i < _circuitHandlers.Length; i++)
153+
{
154+
await _circuitHandlers[i].OnCircuitClosedAsync(Circuit, default);
155+
}
156+
});
157+
158+
_scope.Dispose();
132159
Renderer.Dispose();
133160
}
134161

135162
private void AssertInitialized()
136163
{
137-
if (!_isInitialized)
164+
if (!_initialized)
138165
{
139166
throw new InvalidOperationException("Something is calling into the circuit before Initialize() completes");
140167
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Linq;
56
using Microsoft.AspNetCore.Components.Browser;
67
using Microsoft.AspNetCore.Components.Browser.Rendering;
78
using Microsoft.AspNetCore.Http;
@@ -33,7 +34,7 @@ public override CircuitHost CreateCircuitHost(HttpContext httpContext, IClientPr
3334
{
3435
if (!_options.StartupActions.TryGetValue(httpContext.Request.Path, out var config))
3536
{
36-
var message = $"Could not find a Blazor startup action for request path {httpContext.Request.Path}";
37+
var message = $"Could not find an ASP.NET Core Components startup action for request path '{httpContext.Request.Path}'.";
3738
throw new InvalidOperationException(message);
3839
}
3940

@@ -43,14 +44,19 @@ public override CircuitHost CreateCircuitHost(HttpContext httpContext, IClientPr
4344
var synchronizationContext = new CircuitSynchronizationContext();
4445
var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client, synchronizationContext);
4546

47+
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
48+
.OrderBy(h => h.Order)
49+
.ToArray();
50+
4651
var circuitHost = new CircuitHost(
4752
scope,
4853
client,
4954
rendererRegistry,
5055
renderer,
5156
config,
5257
jsRuntime,
53-
synchronizationContext);
58+
synchronizationContext,
59+
circuitHandlers);
5460

5561
// Initialize per-circuit data that services need
5662
(circuitHost.Services.GetRequiredService<IJSRuntimeAccessor>() as DefaultJSRuntimeAccessor).JSRuntime = jsRuntime;

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,13 @@
66
using Microsoft.AspNetCore.Components.Builder;
77
using Microsoft.AspNetCore.Http;
88

9-
namespace Microsoft.AspNetCore.Components.Server.Circuits
9+
namespace Microsoft.AspNetCore.Components.Server
1010
{
1111
internal class DefaultCircuitFactoryOptions
1212
{
1313
// During the DI configuration phase, we use Configure<DefaultCircuitFactoryOptions>(...)
1414
// callbacks to build up this dictionary mapping paths to startup actions
15-
public Dictionary<PathString, Action<IComponentsApplicationBuilder>> StartupActions { get; }
16-
17-
public DefaultCircuitFactoryOptions()
18-
{
19-
StartupActions = new Dictionary<PathString, Action<IComponentsApplicationBuilder>>();
20-
}
15+
internal Dictionary<PathString, Action<IComponentsApplicationBuilder>> StartupActions { get; }
16+
= new Dictionary<PathString, Action<IComponentsApplicationBuilder>>();
2117
}
2218
}

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

Lines changed: 1 addition & 1 deletion
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)