Skip to content

Commit 50c31b8

Browse files
CR: Redesign
1 parent f568d11 commit 50c31b8

File tree

3 files changed

+30
-20
lines changed

3 files changed

+30
-20
lines changed

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,35 +104,35 @@ public Task<IEnumerable<string>> PrerenderComponentAsync(Type componentType, Par
104104
});
105105
}
106106

107-
public void Initialize(CancellationToken cancellationToken)
107+
public async Task InitializeAsync(CancellationToken cancellationToken)
108108
{
109-
// This Initialize method is fire-and-forget as far as the caller is concerned, because
110-
// if it was to await completion, it would be blocking the SignalR message loop. This could
111-
// lead to deadlock, e.g., if the init process itself waited for an incoming SignalR message
112-
// such as the result of a JSInterop call.
113-
Renderer.InvokeAsync(async () =>
109+
await Renderer.InvokeAsync(async () =>
114110
{
115111
try
116112
{
117113
SetCurrentCircuitHost(this);
114+
_initialized = true; // We're ready to accept incoming JSInterop calls from here on
118115

116+
await OnCircuitOpenedAsync(cancellationToken);
117+
await OnConnectionUpAsync(cancellationToken);
118+
119+
// We add the root components *after* the circuit is flagged as open.
120+
// That's because AddComponentAsync waits for quiescence, which can take
121+
// arbitrarily long. In the meantime we might need to be receiving and
122+
// processing incoming JSInterop calls or similar.
119123
for (var i = 0; i < Descriptors.Count; i++)
120124
{
121125
var (componentType, domElementSelector) = Descriptors[i];
122126
await Renderer.AddComponentAsync(componentType, domElementSelector);
123127
}
124-
125-
await OnCircuitOpenedAsync(cancellationToken);
126-
127-
await OnConnectionUpAsync(cancellationToken);
128128
}
129129
catch (Exception ex)
130130
{
131+
// We have to handle all our own errors here, because the upstream caller
132+
// has to fire-and-forget this
131133
Renderer_UnhandledException(this, ex);
132134
}
133135
});
134-
135-
_initialized = true;
136136
}
137137

138138
public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)

src/Components/Server/src/ComponentHub.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ public string StartCircuit(string uriAbsolute, string baseUriAbsolute)
7676

7777
circuitHost.UnhandledException += CircuitHost_UnhandledException;
7878

79-
circuitHost.Initialize(Context.ConnectionAborted);
79+
// Fire-and-forget the initialization process, because we can't block the
80+
// SignalR message loop (we'd get a deadlock if any of the initialization
81+
// logic relied on receiving a subsequent message from SignalR), and it will
82+
// take care of its own errors anyway.
83+
_ = circuitHost.InitializeAsync(Context.ConnectionAborted);
8084

8185
_circuitRegistry.Register(circuitHost);
8286

src/Components/Server/test/Circuits/CircuitHostTest.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public async Task DisposeAsync_DisposesResources()
4040
}
4141

4242
[Fact]
43-
public void Initialize_InvokesHandlers()
43+
public async Task Initialize_InvokesHandlers()
4444
{
4545
// Arrange
4646
var cancellationToken = new CancellationToken();
@@ -75,15 +75,15 @@ public void Initialize_InvokesHandlers()
7575
var circuitHost = TestCircuitHost.Create(handlers: new[] { handler1.Object, handler2.Object });
7676

7777
// Act
78-
circuitHost.Initialize(cancellationToken);
78+
await circuitHost.InitializeAsync(cancellationToken);
7979

8080
// Assert
8181
handler1.VerifyAll();
8282
handler2.VerifyAll();
8383
}
8484

8585
[Fact]
86-
public void Initialize_ReportsAsyncExceptions()
86+
public async Task Initialize_ReportsOwnAsyncExceptions()
8787
{
8888
// Arrange
8989
var handler = new Mock<CircuitHandler>(MockBehavior.Strict);
@@ -103,15 +103,21 @@ public void Initialize_ReportsAsyncExceptions()
103103
};
104104

105105
// Act
106-
circuitHost.Initialize(new CancellationToken());
107-
handler.VerifyAll();
106+
var initializeAsyncTask = circuitHost.InitializeAsync(new CancellationToken());
108107

109-
// Assert: there was no synchronous exception
108+
// Assert: No synchronous exceptions
109+
handler.VerifyAll();
110110
Assert.Empty(reportedErrors);
111111

112-
// Act/Assert: if the handler throws later, that gets reported
112+
// Act: Trigger async exception
113113
var ex = new InvalidTimeZoneException();
114114
tcs.SetException(ex);
115+
116+
// Assert: The top-level task still succeeds, because the intended usage
117+
// pattern is fire-and-forget.
118+
await initializeAsyncTask;
119+
120+
// Assert: The async exception was reported via the side-channel
115121
Assert.Same(ex, reportedErrors.Single().ExceptionObject);
116122
Assert.False(reportedErrors.Single().IsTerminating);
117123
}

0 commit comments

Comments
 (0)