Skip to content

Commit acba8a5

Browse files
authored
Server-triggered circuit pause section updates (#37146)
1 parent 66fadc1 commit acba8a5

5 files changed

Lines changed: 55 additions & 51 deletions

File tree

aspnetcore/blazor/state-management/server.md

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -197,16 +197,16 @@ This feature is useful in the following scenarios:
197197

198198
`Circuit.RequestCircuitPauseAsync(CancellationToken)` is used to request that the connected client begin the graceful circuit-pause flow. The `CancellationToken` cancels the request before it is accepted by the framework. The method returns `true` if the request was accepted and the client was asked to begin pausing.
199199

200-
<!-- UPDATE 11.0 - REVIEWER NOTE ... The following example might not be what we show.
201-
It's placed here as a possible example
202-
based on Javier's original non-RequestCircuitPauseAsync
203-
approach from the issue. If we use this example,
204-
we need some changes to this.
200+
<!-- UPDATE 11.0 - API doc cross-links
201+
202+
<xref:Microsoft.AspNetCore.Components.Server.Circuits.Circuit.RequestCircuitPauseAsync%2A>
203+
204+
-->
205205
206206
When a server-side Blazor application shuts down (for example, during deployment), connected clients lose their SignalR connection. The approach in this section:
207207

208208
* Detects shutdown before the server closes connections.
209-
* Triggers a pause on connected circuits.
209+
* Triggers a pause on connected circuits via `Microsoft.AspNetCore.Components.Server.Circuits.Circuit.RequestCircuitPauseAsync`.
210210
* Preserves state using [`[PersistentState]` attribute](xref:Microsoft.AspNetCore.Components.PersistentStateAttribute) on component properties.
211211

212212
In the following example implementation, the following code files are placed in a `Shutdown` folder at the root of the app:
@@ -226,18 +226,21 @@ public class ShutdownCircuitOptions
226226
}
227227
```
228228

229+
Using the following approach, the fact that the code sends the `RequestCircuitPauseAsync` asynchronously doesn't mean that upon returning the value that the client is already paused. It's only a request to pause the client, which the client can defer. That's why the code includes the <xref:System.Threading.Tasks.TaskCompletionSource> (`_shutdownTcs`), which is set when there aren't any circuits connected (all of them are successfully shut down). In case a client requests a deferral longer than the server allows, longer than `ShutdownTimeout`, the client doesn't persist state and experiences a normal connection loss. Other clients that don't defer the pause request have their connections re-established after the app goes back online with state persisted.
230+
229231
`Shutdown/CircuitShutdownService.cs`:
230232

231233
```csharp
232234
using System.Collections.Concurrent;
235+
using Microsoft.AspNetCore.Components.Server.Circuits;
233236
using Microsoft.Extensions.Options;
234237

235238
namespace PauseResumeOnShutdown.Shutdown;
236239

237240
public class CircuitShutdownService
238241
{
239-
private readonly ConcurrentDictionary<string, ShutdownCircuitHandler>
240-
_handlers = new();
242+
private readonly ConcurrentDictionary<string, Circuit>
243+
_circuits = new();
241244
private readonly ShutdownCircuitOptions _options;
242245
private bool _isShuttingDown;
243246
private TaskCompletionSource _shutdownTcs = new();
@@ -253,29 +256,30 @@ public class CircuitShutdownService
253256
{
254257
_isShuttingDown = true;
255258

256-
if (_handlers.IsEmpty)
259+
if (_circuits.IsEmpty)
257260
{
258261
return;
259262
}
260263

261-
foreach (var handler in _handlers.Values)
262-
{
263-
handler.Pause();
264-
}
264+
var pauseTasks = _circuits.Values
265+
.Select(c => c.RequestCircuitPauseAsync().AsTask())
266+
.Append(_shutdownTcs.Task);
267+
268+
Task.WhenAll(pauseTasks).Wait(_options.ShutdownTimeout);
265269

266270
_shutdownTcs.Task.Wait(_options.ShutdownTimeout);
267271
}
268272

269-
public void Register(string circuitId, ShutdownCircuitHandler handler)
273+
public void Register(string circuitId, Circuit circuit)
270274
{
271-
_handlers.TryAdd(circuitId, handler);
275+
_circuits.TryAdd(circuitId, circuit);
272276
}
273277

274278
public void Unregister(string circuitId)
275279
{
276-
_handlers.TryRemove(circuitId, out _);
280+
_circuits.TryRemove(circuitId, out _);
277281

278-
if (_isShuttingDown && _handlers.IsEmpty)
282+
if (_isShuttingDown && _circuits.IsEmpty)
279283
{
280284
_shutdownTcs.TrySetResult();
281285
}
@@ -287,18 +291,16 @@ public class CircuitShutdownService
287291

288292
```csharp
289293
using Microsoft.AspNetCore.Components.Server.Circuits;
290-
using Microsoft.JSInterop;
291294

292295
namespace PauseResumeOnShutdown.Shutdown;
293296

294-
public class ShutdownCircuitHandler(
295-
CircuitShutdownService shutdownService,
296-
IJSRuntime jsRuntime) : CircuitHandler
297+
public class ShutdownCircuitHandler(CircuitShutdownService shutdownService)
298+
: CircuitHandler
297299
{
298300
public override Task OnConnectionUpAsync(Circuit circuit,
299301
CancellationToken cancellationToken)
300302
{
301-
shutdownService.Register(circuit.Id, this);
303+
shutdownService.Register(circuit.Id, circuit);
302304

303305
return Task.CompletedTask;
304306
}
@@ -310,22 +312,6 @@ public class ShutdownCircuitHandler(
310312

311313
return Task.CompletedTask;
312314
}
313-
314-
public void Pause()
315-
{
316-
_ = PauseCore();
317-
}
318-
319-
private async Task PauseCore()
320-
{
321-
try
322-
{
323-
await jsRuntime.InvokeVoidAsync("remotePause");
324-
}
325-
catch
326-
{
327-
}
328-
}
329315
}
330316
```
331317

@@ -340,12 +326,12 @@ using PauseResumeOnShutdown.Shutdown;
340326
var builder = WebApplication.CreateBuilder(args);
341327

342328
// Increase host shutdown timeout to allow time for pause operations
343-
// Default value is 10 seconds
329+
// Must be greater than `ShutdownTimeout` in `ShutdownCircuitOptions`
330+
// otherwise the host terminates connections before circuits finish
331+
// pausing
344332
builder.Host.ConfigureHostOptions(options =>
345333
options.ShutdownTimeout = TimeSpan.FromSeconds(30));
346334

347-
// Set circuit shutdown timeout to allow time for the host to restart
348-
// Default value is 10 seconds per 'Shutdown/ShutdownCircuitOptions.cs'
349335
builder.Services.Configure<ShutdownCircuitOptions>(options =>
350336
options.ShutdownTimeout = TimeSpan.FromSeconds(10));
351337

@@ -364,19 +350,25 @@ var app = builder.Build();
364350
// ... rest of pipeline
365351
```
366352

367-
In `App.razor` after the [server-side Blazor script reference](xref:blazor/project-structure#location-of-the-blazor-script), `window.remotePause` is called from the server to trigger pause and returns immediately to avoid a "`Cannot send data`" error when Blazor attempts to send the response back.
353+
Optionally, to defer pause on the client until critical work completes (for example, an in-flight payment), configure the `onPauseRequested` callback in the [Blazor startup configuration](xref:blazor/fundamentals/startup). Place the following after the [server-side Blazor script reference](xref:blazor/project-structure#location-of-the-blazor-script):
368354

369355
```razor
370356
<script>
371-
window.remotePause = function () {
372-
Blazor.pauseCircuit();
373-
};
357+
Blazor.start({
358+
circuit: {
359+
onPauseRequested: async () => {
360+
// Perform any critical cleanup or wait for in-flight operations.
361+
// Return true to allow the pause or false to reject it.
362+
return true;
363+
}
364+
}
365+
});
374366
</script>
375367
```
376368

377-
The `remotePause` function must not be `async` and must not return a value. If it returns a `Promise`, Blazor attempts to send the result back after the connection closes, which results in an error.
369+
Without the `onPauseRequested` callback, the client pauses immediately when the server requests it.
378370

379-
In a component, use the [`[PersistentState]` attribute](xref:Microsoft.AspNetCore.Components.PersistentStateAttribute) to persist component state across pause/resume. In the following `Counter` component example, the current count (`CurrentCount`) is preserved:
371+
In a component, use the [`[PersistentState]` attribute](xref:Microsoft.AspNetCore.Components.PersistentStateAttribute) to persist component state across pause/resume. In the following `Counter` component example, the current count (`CurrentCount`) is preserved across server restarts using the preceding approach:
380372

381373
```razor
382374
@page "/counter"

aspnetcore/host-and-deploy/health-checks.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,13 @@ The `DbContext` check confirms that the app can communicate with the database co
183183
* Use [Entity Framework (EF) Core](/ef/core/).
184184
* Include a package reference to the [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore) NuGet package.
185185

186-
<xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkCoreHealthChecksBuilderExtensions.AddDbContextCheck%2A> registers a health check for a <xref:Microsoft.EntityFrameworkCore.DbContext>. The `DbContext` is supplied to the method as the `TContext`. An overload is available to configure the failure status, tags, and a custom test query.
186+
<!-- HOLD: https://github.com/dotnet/AspNetCore.Docs/issues/37147
187+
188+
<xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkCoreHealthChecksBuilderExtensions.AddDbContextCheck%2A>
189+
190+
-->
191+
192+
`AddDbContextCheck` registers a health check for a <xref:Microsoft.EntityFrameworkCore.DbContext>. The `DbContext` is supplied to the method as the `TContext`. An overload is available to configure the failure status, tags, and a custom test query.
187193

188194
By default:
189195

aspnetcore/host-and-deploy/health-checks/includes/health-checks6-7.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,13 @@ The `DbContext` check confirms that the app can communicate with the database co
166166
* Use [Entity Framework (EF) Core](/ef/core/).
167167
* Include a package reference to the [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore) NuGet package.
168168

169-
<xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkCoreHealthChecksBuilderExtensions.AddDbContextCheck%2A> registers a health check for a <xref:Microsoft.EntityFrameworkCore.DbContext>. The `DbContext` is supplied to the method as the `TContext`. An overload is available to configure the failure status, tags, and a custom test query.
169+
<!-- HOLD: https://github.com/dotnet/AspNetCore.Docs/issues/37147
170+
171+
<xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkCoreHealthChecksBuilderExtensions.AddDbContextCheck%2A>
172+
173+
-->
174+
175+
`AddDbContextCheck` registers a health check for a <xref:Microsoft.EntityFrameworkCore.DbContext>. The `DbContext` is supplied to the method as the `TContext`. An overload is available to configure the failure status, tags, and a custom test query.
170176

171177
By default:
172178

aspnetcore/release-notes/aspnetcore-11.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ author: wadepickett
55
description: Learn about the new features in ASP.NET Core in .NET 11.
66
ms.author: wpickett
77
ms.custom: mvc
8-
ms.date: 05/12/2026
8+
ms.date: 05/13/2026
99
uid: aspnetcore-11
1010
---
1111
# What's new in ASP.NET Core in .NET 11

aspnetcore/release-notes/aspnetcore-11/includes/blazor.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ This feature is useful in the following scenarios:
176176
* Instance draining.
177177
* App maintenance windows.
178178

179-
For more information, see <xref:blazor/state-management/server#server-triggered-circuit-pause>.
179+
For more information and an implementation example for server restarts, see <xref:blazor/state-management/server#server-triggered-circuit-pause>.
180180

181181
<!-- Waiting for content from Marek or a link to the work that was done.
182182

0 commit comments

Comments
 (0)