You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Blazor already supports graceful circuit pause/resume, but the supported initiation path is currently client-driven:
Blazor.pauseCircuit()
Blazor.resumeCircuit()
What is missing is the symmetric server-side capability:
the server should be able to request that a connected circuit begin the existing graceful pause flow
This matters for scenarios such as:
planned shutdowns and deployments,
instance draining,
maintenance windows,
app-driven “preserve state before disconnect” workflows.
Existing framework building blocks
The feature is not starting from scratch. The current codebase already contains:
Client pause/resume APIs
Blazor.pauseCircuit()
Blazor.resumeCircuit()
Hub endpoints
ComponentHub.PauseCircuit()
ComponentHub.ResumeCircuit(...)
Pause/persistence pipeline
CircuitRegistry.PauseCircuitAsync(...)
CircuitPersistenceManager.PauseCircuitAsync(...)
CircuitHost.SendPersistedStateToClient(...)
Paused-session UX
the reconnect UI already has a paused state and resume action
The missing work is therefore primarily public API surface and orchestration.
Goals
Add a first-class server-side pause request API on Circuit.
Keep the client as the actor that performs the pause and owns the UX.
Preserve the existing graceful/ungraceful resume model.
Let apps explicitly choose when to trigger pause.
Make the return contract small and predictable.
Non-goals
No automatic inactivity detection.
No framework decision-making about when an app “should” pause a session.
No guarantee that the same in-memory circuit survives; resume creates a new circuit from persisted state.
No rich synchronous payload return from the API.
Proposed public API
namespaceMicrosoft.AspNetCore.Components.Server.Circuits;publicsealedclassCircuit{/// <summary>/// Requests that the connected client begin the graceful circuit-pause flow./// </summary>/// <param name="cancellationToken">/// Cancels the request before it is accepted by the framework./// </param>/// <returns>/// <see langword="true"/> if the request was accepted and the client was asked to begin pausing;/// otherwise <see langword="false"/>./// </returns>publicValueTask<bool>RequestCircuitPauseAsync(CancellationTokencancellationToken=default);}
Resolved design decisions
1. Should pause be explicit or automatic?
Applications explicitly decide when to call RequestCircuitPauseAsync().
This is the preferred model because:
comparable frameworks generally provide reconnect/reload behavior, not automatic session-pausing logic,
Blazor already exposes an explicit client-side Blazor.pauseCircuit() API,
the server-side API is best understood as the symmetric explicit counterpart to the existing client API.
2. What should the return type and semantics be?
The API returns a boolean because it is only answering the narrow question:
was the pause request accepted and the client asked to begin the flow?
That is the right contract because:
the operation spans multiple asynchronous steps and network roundtrips, so a synchronous return value cannot reliably represent final completion,
the normal flow does not need to hand app code a rich payload because persisted state is already managed through the existing client-held or server-held resume paths,
a small acceptance/rejection result is easier to reason about and keeps the public API aligned with the actual boundary of responsibility.
In this model:
true means the circuit was eligible and the request was accepted,
false means the circuit was not in a state where graceful pause could begin.
3. How is completion observed?
Completion should be observed through CircuitHandlerbecause Blazor already has a supported lifecycle surface for circuit state transitions. Adding a second, pause-specific completion API would duplicate that model.
The existing lifecycle already exposes the events app authors care about:
OnCircuitOpenedAsync
OnConnectionUpAsync
OnConnectionDownAsync
OnCircuitClosedAsync
So applications can observe pause progression by implementing a CircuitHandler and watching:
OnConnectionDownAsync when the client disconnects as part of the pause flow
OnCircuitClosedAsync when the old circuit is finally discarded
This keeps the API small while reusing a lifecycle hook that developers already understand and can register in DI today.
4. What is the fallback behavior if graceful pause does not complete?
If the pause handshake begins but cannot complete cleanly, the framework should use this fallback order:
graceful client-held pause,
server-side persistence fallback if available,
normal reconnect/reload/remount behavior if neither succeeds.
This matches the current Blazor pause/resume architecture and the common pattern in other comparable frameworks.
5. Where is RequestCircuitPauseAsync() valid to call from?
The framework should validate circuit state, not caller location.
That is the better rule because:
enforcing “only callable from CircuitHandler” would be brittle and artificial once a caller legitimately has a Circuit reference,
framework correctness depends on whether the circuit can safely enter the transition, not on which method happened to initiate it,
state-based validation is easier to document, test, and keep stable over time.
RequestCircuitPauseAsync() is valid from any code that legitimately holds a Circuit reference, provided all of the following checks pass:
The circuit is still alive
The underlying CircuitHost still exists and has not been disposed or permanently closed.
The circuit is currently connected
A graceful pause requires an active client connection that can receive the request and drive the browser-side flow.
The circuit is eligible for pause
The circuit is not already fully paused, or the call is treated idempotently while a pause is already in progress.
The transition can be entered safely
The request is serialized onto the circuit’s normal dispatcher/connection-transition path so it cannot race unsafely with reconnect, disconnect, or disposal.
6. How should active circuits be tracked?
Applications should track active circuits via CircuitHandler.
No dedicated framework “pause registry” is required for the initial design. A common pattern is:
register a CircuitHandler,
record circuits on OnConnectionUpAsync,
remove them on OnConnectionDownAsync / OnCircuitClosedAsync,
call RequestCircuitPauseAsync() from app-managed shutdown or drain logic.
This is preferable because the framework already provides the lifecycle hooks needed to maintain that set, and the application is the only layer that knows which circuits it wants to pause and when.
7. What are the concurrency and idempotency rules?
The operation should be idempotent and support only one active connection-state transition at a time.
This is the right behavior because shutdown/drain code may fan out pause requests broadly, and repeated requests should not create spurious failures or race-sensitive semantics.
Practical behavior:
repeated requests while pause is already in progress may return true,
requests on circuits that are already gone or no longer eligible return false,
reconnect/disconnect/pause transitions are serialized through the existing circuit execution model.
8. Should the existing paused-session UI be reused?
The existing reconnect dialog should be reused for server-requested pause because the user concept is the same: the session is temporarily unavailable, but a resume path exists.
Reusing it avoids inventing a second UI model for the same state transition and keeps the first version lower-risk and more consistent.
The dialog should:
show the paused state/message,
allow resume through the existing flow,
optionally allow customization later without requiring it for the first version.
9. What telemetry and logging should exist?
The framework should emit explicit events for the key milestones of the flow so acceptance, fallback, and completion can be diagnosed separately.
This is especially important because a boolean return only tells the caller whether the request was accepted; it does not explain what happened afterward.
Useful events include:
pause requested,
pause accepted,
pause rejected,
persisted state sent to client,
fallback to server-side persistence,
pause completed,
pause timed out / failed,
resume succeeded / rejected.
Proposed runtime flow
App code decides a circuit should pause and calls RequestCircuitPauseAsync().
If the circuit is alive, connected, and eligible, the framework accepts the request and returns true.
The framework sends a dedicated server-to-client message over the existing circuit connection.
The browser-side circuit manager:
shows the existing paused-session UI,
performs the current internal pause path,
persists state,
disconnects cleanly.
If the client-held graceful path fails, the framework falls back to server-side persistence when possible.
Later, resume proceeds through the existing client-led Blazor.resumeCircuit() + ResumeCircuit(...) flow.
If no persisted state is available, normal reload/remount behavior applies.
sequenceDiagram
participant App as App Code
participant FW as Framework<br/>(CircuitHost)
participant SR as SignalR
participant Client as Browser<br/>(CircuitManager)
participant UI as Reconnect UI
Note over App,UI: Server-triggered circuit pause flow
App->>FW: RequestCircuitPauseAsync()
FW->>FW: Validate: alive? connected? eligible?
alt Circuit not eligible
FW-->>App: return false
else Circuit eligible
FW-->>App: return true
FW->>SR: Send JS.RequestPause
SR->>Client: JS.RequestPause
Client->>UI: Show paused-session dialog
Client->>SR: PauseCircuit (hub call)
SR->>FW: PauseCircuit()
FW->>FW: Remove from ConnectedCircuits
FW->>FW: Persist state (ComponentStatePersistenceManager)
FW->>SR: JS.SavePersistedState
SR->>Client: JS.SavePersistedState
alt Client stores state successfully
Client->>Client: Store persisted state
else Client push fails
FW->>FW: Fallback: server-side PersistCircuitAsync
end
Client->>SR: disconnect()
SR->>FW: OnClose
FW->>FW: CircuitHandler.OnConnectionDownAsync
FW->>FW: DisposeAsync (components)
FW->>FW: CircuitHandler.OnCircuitClosedAsync
Note over App,UI: Later — user clicks Resume
Client->>SR: New connection
Client->>SR: ResumeCircuit(circuitId, state)
SR->>FW: ResumeCircuit()
FW->>FW: Create new circuit from persisted state
FW->>SR: Initial render
SR->>Client: JS.RenderBatch
Client->>UI: Hide dialog, show app
end
Related issue: #62327
Problem
Blazor already supports graceful circuit pause/resume, but the supported initiation path is currently client-driven:
Blazor.pauseCircuit()Blazor.resumeCircuit()What is missing is the symmetric server-side capability:
This matters for scenarios such as:
Existing framework building blocks
The feature is not starting from scratch. The current codebase already contains:
Client pause/resume APIs
Blazor.pauseCircuit()Blazor.resumeCircuit()Hub endpoints
ComponentHub.PauseCircuit()ComponentHub.ResumeCircuit(...)Pause/persistence pipeline
CircuitRegistry.PauseCircuitAsync(...)CircuitPersistenceManager.PauseCircuitAsync(...)CircuitHost.SendPersistedStateToClient(...)Paused-session UX
The missing work is therefore primarily public API surface and orchestration.
Goals
Circuit.Non-goals
Proposed public API
Resolved design decisions
1. Should pause be explicit or automatic?
Applications explicitly decide when to call
RequestCircuitPauseAsync().This is the preferred model because:
Blazor.pauseCircuit()API,2. What should the return type and semantics be?
The API returns a boolean because it is only answering the narrow question:
That is the right contract because:
In this model:
truemeans the circuit was eligible and the request was accepted,falsemeans the circuit was not in a state where graceful pause could begin.3. How is completion observed?
Completion should be observed through
CircuitHandlerbecause Blazor already has a supported lifecycle surface for circuit state transitions. Adding a second, pause-specific completion API would duplicate that model.The existing lifecycle already exposes the events app authors care about:
OnCircuitOpenedAsyncOnConnectionUpAsyncOnConnectionDownAsyncOnCircuitClosedAsyncSo applications can observe pause progression by implementing a
CircuitHandlerand watching:OnConnectionDownAsyncwhen the client disconnects as part of the pause flowOnCircuitClosedAsyncwhen the old circuit is finally discardedThis keeps the API small while reusing a lifecycle hook that developers already understand and can register in DI today.
4. What is the fallback behavior if graceful pause does not complete?
If the pause handshake begins but cannot complete cleanly, the framework should use this fallback order:
This matches the current Blazor pause/resume architecture and the common pattern in other comparable frameworks.
5. Where is
RequestCircuitPauseAsync()valid to call from?The framework should validate circuit state, not caller location.
That is the better rule because:
CircuitHandler” would be brittle and artificial once a caller legitimately has aCircuitreference,RequestCircuitPauseAsync()is valid from any code that legitimately holds aCircuitreference, provided all of the following checks pass:The circuit is still alive
The underlying
CircuitHoststill exists and has not been disposed or permanently closed.The circuit is currently connected
A graceful pause requires an active client connection that can receive the request and drive the browser-side flow.
The circuit is eligible for pause
The circuit is not already fully paused, or the call is treated idempotently while a pause is already in progress.
The transition can be entered safely
The request is serialized onto the circuit’s normal dispatcher/connection-transition path so it cannot race unsafely with reconnect, disconnect, or disposal.
6. How should active circuits be tracked?
Applications should track active circuits via
CircuitHandler.No dedicated framework “pause registry” is required for the initial design. A common pattern is:
CircuitHandler,OnConnectionUpAsync,OnConnectionDownAsync/OnCircuitClosedAsync,RequestCircuitPauseAsync()from app-managed shutdown or drain logic.This is preferable because the framework already provides the lifecycle hooks needed to maintain that set, and the application is the only layer that knows which circuits it wants to pause and when.
7. What are the concurrency and idempotency rules?
The operation should be idempotent and support only one active connection-state transition at a time.
This is the right behavior because shutdown/drain code may fan out pause requests broadly, and repeated requests should not create spurious failures or race-sensitive semantics.
Practical behavior:
true,false,8. Should the existing paused-session UI be reused?
The existing reconnect dialog should be reused for server-requested pause because the user concept is the same: the session is temporarily unavailable, but a resume path exists.
Reusing it avoids inventing a second UI model for the same state transition and keeps the first version lower-risk and more consistent.
The dialog should:
9. What telemetry and logging should exist?
The framework should emit explicit events for the key milestones of the flow so acceptance, fallback, and completion can be diagnosed separately.
This is especially important because a boolean return only tells the caller whether the request was accepted; it does not explain what happened afterward.
Useful events include:
Proposed runtime flow
RequestCircuitPauseAsync().true.Blazor.resumeCircuit()+ResumeCircuit(...)flow.sequenceDiagram participant App as App Code participant FW as Framework<br/>(CircuitHost) participant SR as SignalR participant Client as Browser<br/>(CircuitManager) participant UI as Reconnect UI Note over App,UI: Server-triggered circuit pause flow App->>FW: RequestCircuitPauseAsync() FW->>FW: Validate: alive? connected? eligible? alt Circuit not eligible FW-->>App: return false else Circuit eligible FW-->>App: return true FW->>SR: Send JS.RequestPause SR->>Client: JS.RequestPause Client->>UI: Show paused-session dialog Client->>SR: PauseCircuit (hub call) SR->>FW: PauseCircuit() FW->>FW: Remove from ConnectedCircuits FW->>FW: Persist state (ComponentStatePersistenceManager) FW->>SR: JS.SavePersistedState SR->>Client: JS.SavePersistedState alt Client stores state successfully Client->>Client: Store persisted state else Client push fails FW->>FW: Fallback: server-side PersistCircuitAsync end Client->>SR: disconnect() SR->>FW: OnClose FW->>FW: CircuitHandler.OnConnectionDownAsync FW->>FW: DisposeAsync (components) FW->>FW: CircuitHandler.OnCircuitClosedAsync Note over App,UI: Later — user clicks Resume Client->>SR: New connection Client->>SR: ResumeCircuit(circuitId, state) SR->>FW: ResumeCircuit() FW->>FW: Create new circuit from persisted state FW->>SR: Initial render SR->>Client: JS.RenderBatch Client->>UI: Hide dialog, show app end