Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 39 additions & 47 deletions aspnetcore/blazor/state-management/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- UPDATE 11.0 - REVIEWER NOTE ... The following example might not be what we show.
It's placed here as a possible example
based on Javier's original non-RequestCircuitPauseAsync
approach from the issue. If we use this example,
we need some changes to this.
<!-- UPDATE 11.0 - API doc cross-links

<xref:Microsoft.AspNetCore.Components.Server.Circuits.Circuit.RequestCircuitPauseAsync%2A>

-->

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:
Expand All @@ -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 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.

`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<string, ShutdownCircuitHandler>
_handlers = new();
private readonly ConcurrentDictionary<string, Circuit>
_circuits = new();
private readonly ShutdownCircuitOptions _options;
private bool _isShuttingDown;
private TaskCompletionSource _shutdownTcs = new();
Expand All @@ -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, circuit);
}

public void Unregister(string circuitId)
{
_handlers.TryRemove(circuitId, out _);
_circuits.TryRemove(circuitId, out _);

if (_isShuttingDown && _handlers.IsEmpty)
if (_isShuttingDown && _circuits.IsEmpty)
{
_shutdownTcs.TrySetResult();
}
Expand All @@ -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;
}
Expand All @@ -310,22 +312,6 @@ public class ShutdownCircuitHandler(

return Task.CompletedTask;
}

public void Pause()
{
_ = PauseCore();
}

private async Task PauseCore()
{
try
{
await jsRuntime.InvokeVoidAsync("remotePause");
}
catch
{
}
}
}
```

Expand All @@ -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<ShutdownCircuitOptions>(options =>
options.ShutdownTimeout = TimeSpan.FromSeconds(10));

Expand All @@ -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
<script>
window.remotePause = function () {
Blazor.pauseCircuit();
};
Blazor.start({
circuit: {
onPauseRequested: async () => {
// Perform any critical cleanup or wait for in-flight operations.
// Return true to allow the pause or false to reject it.
return true;
}
}
});
</script>
```

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"
Expand Down
8 changes: 7 additions & 1 deletion aspnetcore/host-and-deploy/health-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,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.

<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.
<!-- HOLD: https://github.com/dotnet/AspNetCore.Docs/issues/37147

<xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkCoreHealthChecksBuilderExtensions.AddDbContextCheck%2A>

-->

`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.

By default:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<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.
<!-- HOLD: https://github.com/dotnet/AspNetCore.Docs/issues/37147

<xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkCoreHealthChecksBuilderExtensions.AddDbContextCheck%2A>

-->

`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.

By default:

Expand Down
2 changes: 1 addition & 1 deletion aspnetcore/release-notes/aspnetcore-11.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ author: wadepickett
description: Learn about the new features in ASP.NET Core in .NET 11.
ms.author: wpickett
ms.custom: mvc
ms.date: 05/12/2026
ms.date: 05/13/2026
uid: aspnetcore-11
---
# What's new in ASP.NET Core in .NET 11
Expand Down
2 changes: 1 addition & 1 deletion aspnetcore/release-notes/aspnetcore-11/includes/blazor.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ This feature is useful in the following scenarios:
* Instance draining.
* App maintenance windows.

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

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

Expand Down
Loading