From 001f5359ee1cdca4aa61ab379ae8a675e422ba71 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 13 May 2026 13:28:30 -0400 Subject: [PATCH 1/6] Server-triggered circuit pause section updates --- aspnetcore/blazor/state-management/server.md | 86 +++++++++---------- .../aspnetcore-11/includes/blazor.md | 2 +- 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/aspnetcore/blazor/state-management/server.md b/aspnetcore/blazor/state-management/server.md index e89e31547121..45cd2b69cd91 100644 --- a/aspnetcore/blazor/state-management/server.md +++ b/aspnetcore/blazor/state-management/server.md @@ -197,16 +197,16 @@ This feature is useful in the following scenarios: `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. - When a server-side Blazor application shuts down (for example, during deployment), connected clients lose their SignalR connection. The approach in this section: * Detects shutdown before the server closes connections. -* Triggers a pause on connected circuits. +* Triggers a pause on connected circuits via `Microsoft.AspNetCore.Components.Server.Circuits.Circuit.RequestCircuitPauseAsync`. * Preserves state using [`[PersistentState]` attribute](xref:Microsoft.AspNetCore.Components.PersistentStateAttribute) on component properties. 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 } ``` +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 that client, which the client can defer. That's why the code includes `_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 server allows, a period longer than `ShutdownTimeout`, that client doesn't have their state persisted and experiences a normal connection loss. + `Shutdown/CircuitShutdownService.cs`: ```csharp using System.Collections.Concurrent; +using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.Extensions.Options; namespace PauseResumeOnShutdown.Shutdown; public class CircuitShutdownService { - private readonly ConcurrentDictionary - _handlers = new(); + private readonly ConcurrentDictionary + _circuits = new(); private readonly ShutdownCircuitOptions _options; private bool _isShuttingDown; private TaskCompletionSource _shutdownTcs = new(); @@ -253,29 +256,30 @@ public class CircuitShutdownService { _isShuttingDown = true; - if (_handlers.IsEmpty) + if (_circuits.IsEmpty) { return; } - foreach (var handler in _handlers.Values) - { - handler.Pause(); - } + var pauseTasks = _circuits.Values + .Select(c => c.RequestCircuitPauseAsync().AsTask()) + .Append(_shutdownTcs.Task); + + Task.WhenAll(pauseTasks).Wait(_options.ShutdownTimeout); _shutdownTcs.Task.Wait(_options.ShutdownTimeout); } - public void Register(string circuitId, ShutdownCircuitHandler handler) + public void Register(string circuitId, Circuit circuit) { - _handlers.TryAdd(circuitId, handler); + _circuits.TryAdd(circuitId, handler); } public void Unregister(string circuitId) { - _handlers.TryRemove(circuitId, out _); + _circuits.TryRemove(circuitId, out _); - if (_isShuttingDown && _handlers.IsEmpty) + if (_isShuttingDown && _circuits.IsEmpty) { _shutdownTcs.TrySetResult(); } @@ -287,18 +291,16 @@ public class CircuitShutdownService ```csharp using Microsoft.AspNetCore.Components.Server.Circuits; -using Microsoft.JSInterop; namespace PauseResumeOnShutdown.Shutdown; -public class ShutdownCircuitHandler( - CircuitShutdownService shutdownService, - IJSRuntime jsRuntime) : CircuitHandler +public class ShutdownCircuitHandler(CircuitShutdownService shutdownService) + : CircuitHandler { public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken) { - shutdownService.Register(circuit.Id, this); + shutdownService.Register(circuit.Id, circuit); return Task.CompletedTask; } @@ -310,22 +312,6 @@ public class ShutdownCircuitHandler( return Task.CompletedTask; } - - public void Pause() - { - _ = PauseCore(); - } - - private async Task PauseCore() - { - try - { - await jsRuntime.InvokeVoidAsync("remotePause"); - } - catch - { - } - } } ``` @@ -340,12 +326,12 @@ using PauseResumeOnShutdown.Shutdown; var builder = WebApplication.CreateBuilder(args); // Increase host shutdown timeout to allow time for pause operations -// Default value is 10 seconds +// Must be greater than `ShutdownTimeout` in `ShutdownCircuitOptions` +// otherwise the host terminates connections before circuits finish +// pausing builder.Host.ConfigureHostOptions(options => options.ShutdownTimeout = TimeSpan.FromSeconds(30)); -// Set circuit shutdown timeout to allow time for the host to restart -// Default value is 10 seconds per 'Shutdown/ShutdownCircuitOptions.cs' builder.Services.Configure(options => options.ShutdownTimeout = TimeSpan.FromSeconds(10)); @@ -364,19 +350,25 @@ var app = builder.Build(); // ... rest of pipeline ``` -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. +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): ```razor ``` -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. +Without the `onPauseRequested` callback, the client pauses immediately when the server requests it. -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: +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: ```razor @page "/counter" diff --git a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md index d1818c71cac2..61e6fb715ac4 100644 --- a/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-11/includes/blazor.md @@ -176,7 +176,7 @@ This feature is useful in the following scenarios: * Instance draining. * App maintenance windows. -For more information, see . +For more information and an implementation example for server restarts, see . + +`AddDbContextCheck` registers a health check for a . 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. By default: diff --git a/aspnetcore/host-and-deploy/health-checks/includes/health-checks6-7.md b/aspnetcore/host-and-deploy/health-checks/includes/health-checks6-7.md index 80c6967f4ada..0dacadae345b 100644 --- a/aspnetcore/host-and-deploy/health-checks/includes/health-checks6-7.md +++ b/aspnetcore/host-and-deploy/health-checks/includes/health-checks6-7.md @@ -166,7 +166,13 @@ The `DbContext` check confirms that the app can communicate with the database co * Use [Entity Framework (EF) Core](/ef/core/). * Include a package reference to the [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore) NuGet package. - registers a health check for a . 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. + + +`AddDbContextCheck` registers a health check for a . 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. By default: From 16f479510d24618f32f2a1902d400d5b2ab33098 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 13 May 2026 13:57:24 -0400 Subject: [PATCH 4/6] Add API regression revert issue to comments --- aspnetcore/host-and-deploy/health-checks.md | 2 +- .../host-and-deploy/health-checks/includes/health-checks6-7.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnetcore/host-and-deploy/health-checks.md b/aspnetcore/host-and-deploy/health-checks.md index abc7a119f0b1..6cf80f93cbf8 100644 --- a/aspnetcore/host-and-deploy/health-checks.md +++ b/aspnetcore/host-and-deploy/health-checks.md @@ -183,7 +183,7 @@ The `DbContext` check confirms that the app can communicate with the database co * Use [Entity Framework (EF) Core](/ef/core/). * Include a package reference to the [`Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore`](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore) NuGet package. -