Skip to content

feat(SignalR): add Dispatch/Connection services for wasm mode - #1257

Merged
ArgoZhang merged 13 commits into
masterfrom
dev-signalR
Oct 27, 2026
Merged

ArgoZhang merged 13 commits into
masterfrom
dev-signalR

Conversation

@ArgoZhang

@ArgoZhang ArgoZhang commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Link issues

fixes #1254

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Add shared SignalR-backed dispatch and connection services for WebAssembly clients.

New Features:

  • Add WebAssembly SignalR client services for typed message dispatch, subscriptions, heartbeat reporting, and connection snapshots.
  • Provide a shared SignalR Hub connection with automatic reconnect and subscription restoration.

Enhancements:

  • Add server connection snapshot model for reporting active client connections.

Build:

  • Add the new SignalR client project to the solution and package configuration.

@sourcery-ai

sourcery-ai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adds a WebAssembly SignalR client package with a shared reconnecting Hub connection, typed dispatch subscriptions, heartbeat reporting, and connection-snapshot polling, while updating middleware to expose the shared snapshot contract and client-facing Hub behavior.

Sequence diagram for SignalR dispatch subscription and delivery

sequenceDiagram
    participant App as WASM application
    participant Dispatch as DefaultDispatchService
    participant Connection as SignalRConnection
    participant Hub as BootstrapBlazorHub
    participant Subscriber as Local callback

    App->>Dispatch: SubscribeAsync(callback)
    Dispatch->>Connection: SubscribeAsync(entryType, ReceiveAsync)
    Connection->>Hub: Subscribe(entryType)
    Hub-->>Connection: true
    App->>Dispatch: DispatchAsync(entry)
    Dispatch->>Connection: DispatchAsync(entryType, entry)
    Connection->>Hub: Dispatch(entryType, entry)
    Hub-->>Connection: Receive(entryType, JSON)
    Connection->>Dispatch: ReceiveAsync(JSON)
    Dispatch->>Subscriber: callback(entry)
Loading

Sequence diagram for connection heartbeat and snapshot polling

sequenceDiagram
    participant Client as WASM client
    participant Connection as DefaultConnectionService
    participant SignalR as SignalRConnection
    participant Hub as BootstrapBlazorHub
    participant Store as ConnectionHandler

    Client->>Connection: AddOrUpdateAsync(client)
    Connection->>SignalR: InvokeAsync(Heartbeat, client)
    SignalR->>Hub: Heartbeat(client)
    Hub->>Store: ReportHeartbeatAsync(client)
    loop Every 3 seconds
        Connection->>SignalR: InvokeAsync(GetConnectionSnapshot)
        SignalR->>Hub: GetConnectionSnapshot()
        Hub->>Store: GetSnapshotAsync()
        Store-->>Hub: ConnectionSnapshot
        Hub-->>SignalR: ConnectionSnapshot
        SignalR-->>Connection: Update local snapshot
    end
Loading

File-Level Changes

Change Details Files
Adds a WebAssembly-compatible SignalR client package that shares one Hub connection across dispatch and connection services.
  • Introduces DI registration for the shared connection, generic dispatch services, and connection snapshot service.
  • Builds Hub URLs from NavigationManager and adds automatic reconnect, transient-failure classification, operation timeouts, and subscription restoration.
  • Tracks connection usage and transport subscriptions so the Hub starts and stops based on active consumers.
BootstrapBlazor.Extensions.slnx
src/extensions/BootstrapBlazor.SignalR.Client/BootstrapBlazor.SignalR.Client.csproj
src/extensions/BootstrapBlazor.SignalR.Client/ServiceCollectionExtensions.cs
src/extensions/BootstrapBlazor.SignalR.Client/SignalRConnection.cs
Implements client-side dispatch lifecycle, delivery, and cleanup for typed messages.
  • Dispatches entries to the server and local subscribers only after a successful send.
  • Serializes subscribe/unsubscribe operations, supports duplicate callback registrations, retries transient subscription failures, and rolls back failed registrations.
  • Deserializes received payloads, invokes subscribers in order, isolates callback failures, and cleans up pending transport subscriptions during disposal.
src/extensions/BootstrapBlazor.SignalR.Client/DefaultDispatchService.cs
Implements client-side heartbeats and polling of server connection snapshots.
  • Retains the shared SignalR connection while polling snapshots every three seconds.
  • Reports client heartbeats and resets or stops polling when queries are disabled, unauthorized, rejected, or canceled.
  • Exposes the latest count and connection collection and releases the shared connection during asynchronous disposal.
src/extensions/BootstrapBlazor.SignalR.Client/DefaultConnectionService.cs
Moves connection snapshot transport data into a shared public contract and adapts middleware for client polling.
  • Adds the public ConnectionSnapshot record used by both middleware and the new client package.
  • Returns snapshots through the Hub while preserving feature and authorization checks.
  • Moves heartbeat validation responsibility into the connection handling flow and updates project references.
src/middleware/BootstrapBlazor.Middleware/ConnectionSnapshot.cs
src/middleware/BootstrapBlazor.Middleware/BootstrapBlazorHub.cs
src/middleware/BootstrapBlazor.Middleware/ConnectionHandler.cs
src/middleware/BootstrapBlazor.Middleware/BootstrapBlazor.Middleware.csproj

Assessment against linked issues

Issue Objective Addressed Explanation
#1254 Provide WebAssembly-compatible SignalR dispatch services for sending entries and subscribing to server-dispatched messages. ✅
#1254 Provide a WebAssembly-compatible connection service for reporting client heartbeats and retrieving connection snapshots. ✅
#1254 Register the client services through a public setup extension and share a SignalR Hub connection between them. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@bb-auto bb-auto Bot added the enhancement New feature or request label Oct 27, 2026
@bb-auto bb-auto Bot added this to the v10.0.0 milestone Oct 27, 2026
bb-auto[bot]
bb-auto Bot previously approved these changes Oct 27, 2026

@bb-auto bb-auto Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto approved by bb-auto

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 6 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/middleware/BootstrapBlazor.Middleware/ConnectionHandler.cs" line_range="27-30" />
<code_context>
             return false;
         }

-        if (client == null || string.IsNullOrWhiteSpace(client.Id) || client.Id.Length > 128 ||
-            !Enum.IsDefined(typeof(WebClientDeviceType), client.Device))
-        {
-            throw new HubException("Invalid connection heartbeat.");
-        }
-
</code_context>
<issue_to_address>
**Malformed heartbeats enter storage**

When a caller sends a null `ClientInfo`, a blank or oversized ID, or an undefined device value, `ReportHeartbeatAsync` dereferences a null `client` and forwards invalid IDs or device values to `IConnectionService`, so null calls fail and malformed records enter connection storage.

Restore validation of the client, ID, and device value before assigning fields or calling the connection service.
</issue_to_address>

### Comment 2
<location path="src/extensions/BootstrapBlazor.SignalR.Client/SignalRConnection.cs" line_range="204" />
<code_context>
+        try
+        {
+            await EnsureConnectedAsync(token);
+            await _connection.InvokeAsync("Dispatch", entryType, entry, token);
+        }
+        finally
</code_context>
<issue_to_address>
**Slow dispatch blocks shared connection**

When the server's `Dispatch` method or an awaited publish handler stalls, `DispatchAsync` invokes the Hub directly without a timeout, so a server dispatch that never completes holds `_gate` indefinitely. All later operations using the shared connection then wait on that gate, and dispatch callers with the default cancellation token remain stuck.

Apply a bounded timeout to the dispatch invocation, or otherwise ensure a stalled dispatch cannot hold the shared-operation gate indefinitely.
</issue_to_address>

### Comment 3
<location path="src/extensions/BootstrapBlazor.SignalR.Client/SignalRConnection.cs" line_range="64" />
<code_context>
+    {
+        _logger = logger;
+        _connection = new HubConnectionBuilder()
+            .WithUrl(navigation.ToAbsoluteUri(hubPath))
+            .WithAutomaticReconnect(new ConnectionRetryPolicy())
+            .Build();
</code_context>
<issue_to_address>
**Bearer-only Hub calls fail**

When the Hub endpoint requires a bearer token and does not accept browser cookies, `SignalRConnection` builds the Hub URL with `WithUrl` but supplies no access-token provider, and `AddBootstrapBlazorHub` exposes no authentication configuration, so bearer-only WebAssembly clients cannot authorize Hub operations.

Provide an access-token provider and expose the required authentication configuration to Hub clients.

Also at `src/extensions/BootstrapBlazor.SignalR.Client/ServiceCollectionExtensions.cs:22`.
</issue_to_address>

### Comment 4
<location path="src/extensions/BootstrapBlazor.SignalR.Client/SignalRConnection.cs" line_range="175-179" />
<code_context>
+        using var cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _lifetime.Token);
+        cancellation.CancelAfter(TimeSpan.FromSeconds(30));
+        var token = cancellation.Token;
+        await _gate.WaitAsync(token);
+        try
+        {
+            await EnsureConnectedAsync(token);
+            return await _connection.InvokeCoreAsync<TResult>(method, argument == null ? [] : [argument], token);
+        }
+        finally
</code_context>
<issue_to_address>
**Slow snapshots block dispatches**

When a connection snapshot request takes several seconds or reaches its 30-second timeout, `InvokeAsync` holds `_gate` for the entire Hub request, so a slow snapshot poll blocks dispatch, heartbeat, and subscription operations waiting for that gate; a timed-out poll can delay them for up to 30 seconds.

Avoid holding the shared-operation gate while awaiting Hub responses, while retaining synchronization around connection-state changes.
</issue_to_address>

### Comment 5
<location path="src/extensions/BootstrapBlazor.SignalR.Client/ServiceCollectionExtensions.cs" line_range="30" />
<code_context>
+    /// <para lang="zh">服务端需注册并映射对应 Hub,Subscribe 方法需返回 bool,以区分订阅拒绝与可重试的执行失败。连接快照查询需在服务端单独启用并授权客户端访问</para>
+    /// <para lang="en">The server must register and map a corresponding Hub whose Subscribe method returns bool to distinguish subscription rejection from retryable execution failures. Connection snapshot queries require explicit server-side opt-in and client authorization</para>
+    /// </remarks>
+    public static IServiceCollection AddBootstrapBlazorHub(this IServiceCollection services, string hubPath = "bb.hub")
+    {
+#if NET7_0_OR_GREATER
</code_context>
<issue_to_address>
**Hub URL breaks under app base path**

When the WebAssembly app is hosted beneath a non-root base path and the caller uses the default `hubPath`, `NavigationManager.ToAbsoluteUri` resolves the default relative `bb.hub` beneath the WebAssembly app’s base path, while the server maps the default endpoint at `/bb.hub`; the client therefore connects to a nonexistent endpoint and its dispatch, heartbeat, and snapshot operations fail.

Make the client default an absolute-root path (`/bb.hub`) or resolve the configured path consistently with the server endpoint mapping.

Also at `src/extensions/BootstrapBlazor.SignalR.Client/SignalRConnection.cs:64`.
</issue_to_address>

### Comment 6
<location path="src/extensions/BootstrapBlazor.SignalR.Client/DefaultDispatchService.cs" line_range="40" />
<code_context>
+    public async Task DispatchAsync(DispatchEntry<TEntry> entry, CancellationToken cancellationToken = default)
+    {
+        await connection.DispatchAsync(_entryType, entry, cancellationToken);
+        await DispatchCoreAsync(entry, cancellationToken);
+    }
+
</code_context>
<issue_to_address>
**Dispatch callbacks observe messages out of order**

When multiple entries are dispatched concurrently, or receive callbacks overlap, and subscribers rely on entry order, `DispatchAsync` sends each entry through the shared connection, then invokes `DispatchCoreAsync` without serializing that local delivery with other sends; concurrent callers can therefore complete their local callbacks in the opposite order from their server invocations. Concurrent receive callbacks also enter `DispatchCoreAsync` independently, so subscribers can apply later entries before earlier ones and end up with incorrect state.

Serialize entry delivery per service, including the send-to-local-delivery transition, or explicitly provide ordering metadata and have consumers reject stale entries.

Also at `src/extensions/BootstrapBlazor.SignalR.Client/DefaultDispatchService.cs:172`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

# Conflicts:
#	src/middleware/BootstrapBlazor.Middleware/BootstrapBlazor.Middleware.csproj
bb-auto[bot]
bb-auto Bot previously approved these changes Oct 27, 2026

@bb-auto bb-auto Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto approved by bb-auto

@bb-auto bb-auto Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto approved by bb-auto

@ArgoZhang
ArgoZhang merged commit 705d5e1 into master Oct 27, 2026
3 checks passed
@ArgoZhang
ArgoZhang deleted the dev-signalR branch October 27, 2026 05:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(SignalR): add Dispatch/Connection services for wasm mode

1 participant