From 450ad96c417f9f2dfa5b8a5e25b335ffba7fe32d Mon Sep 17 00:00:00 2001 From: PureWeen <223556219+Copilot@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:16:51 -0500 Subject: [PATCH 1/6] Add SignalR contributor guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/instructions/signalr.instructions.md | 7 + docs/README.md | 1 + docs/SignalRGuidance.md | 185 +++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 .github/instructions/signalr.instructions.md create mode 100644 docs/SignalRGuidance.md diff --git a/.github/instructions/signalr.instructions.md b/.github/instructions/signalr.instructions.md new file mode 100644 index 000000000000..a3c55634f9cf --- /dev/null +++ b/.github/instructions/signalr.instructions.md @@ -0,0 +1,7 @@ +--- +description: Instructions for folder 'src\SignalR' +applyTo: "src/SignalR/**" +--- + +For SignalR work, follow the relevant sections of the +[SignalR contributor guidance](../../docs/SignalRGuidance.md). diff --git a/docs/README.md b/docs/README.md index 05b50f3f8afa..089d3efa0c25 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,3 +30,4 @@ The table below outlines the different docs in this folder and what they are hel | [Adding new Projects to the Repo](AddingNewProjects.md) | Outlines the process of adding new projects (i.e. `.csproj` files) to the repo | Anyone who finds themselves trying to add a new project and including it in the build. | | [Using WebTransport in Kestrel](WebTransport.md) | Outlines how to setup Kestrel to use WebTransport | Anyone looking to support WebTransport | | [Benchmarking](Benchmarks.md) | Instructions on how to benchmark PRs and local changes | .NET team | +| [SignalR contributor guidance](SignalRGuidance.md) | Implementation, design, testing, and review guidance for SignalR | Contributors working under `src/SignalR/**` | diff --git a/docs/SignalRGuidance.md b/docs/SignalRGuidance.md new file mode 100644 index 000000000000..eb6fe0eede0b --- /dev/null +++ b/docs/SignalRGuidance.md @@ -0,0 +1,185 @@ +# SignalR contributor guidance + +This guidance covers implementation, design, testing, and review work under `src/SignalR/**`. +Use it with the repository-wide contributor instructions and the documentation already maintained +inside the SignalR tree. + +## Source ownership + +Keep changes in the layer that owns the behavior: + +| Area | Responsibility | +| --- | --- | +| [`server/Core`](../src/SignalR/server/Core) | Hub abstractions, dispatch, connection lifetime, groups, users, filters, and the in-process `HubLifetimeManager`. | +| [`server/SignalR`](../src/SignalR/server/SignalR) | ASP.NET Core DI and endpoint integration, plus server integration tests. | +| [`server/StackExchangeRedis`](../src/SignalR/server/StackExchangeRedis) | Redis scaleout and its cross-server routing tests. | +| [`server/Specification.Tests`](../src/SignalR/server/Specification.Tests) | Reusable conformance tests for `HubLifetimeManager` implementations. | +| [`common/Http.Connections`](../src/SignalR/common/Http.Connections) | Negotiation and the server implementations of WebSockets, Server-Sent Events, Long Polling, and HTTP sends. | +| [`common/SignalR.Common`](../src/SignalR/common/SignalR.Common) | Shared hub messages, handshake support, and related test infrastructure. | +| [`common/Shared`](../src/SignalR/common/Shared) | Shared text and binary framing plus stateful reconnect buffering compiled into multiple projects. | +| [`common/Protocols.*`](../src/SignalR/common) | JSON, MessagePack, and Newtonsoft.Json hub protocol implementations. | +| [`clients`](../src/SignalR/clients) | Independent .NET, TypeScript, and Java clients with different platform and feature capabilities. | +| [`docs/specs`](../src/SignalR/docs/specs) | The Hub Protocol and transport wire specifications. | + +Do not move HTTP transport behavior into hub dispatch, provider-specific scaleout behavior into the +core lifetime manager, or client-specific constraints into shared protocol contracts. Changes that +cross these boundaries should preserve the abstractions between them and include tests at each +affected boundary. + +## Wire contracts and compatibility + +Treat the [Hub Protocol](../src/SignalR/docs/specs/HubProtocol.md) and +[transport protocols](../src/SignalR/docs/specs/TransportProtocols.md) as compatibility contracts. + +- The Hub Protocol assumes reliable, ordered message delivery and does not provide general + retransmission or reordering. +- The client handshake is the first hub-protocol message. It is JSON regardless of the selected hub + protocol and uses the record-separator text framing implemented by `HandshakeProtocol`. +- JSON and MessagePack represent the same logical message families, but their wire encodings are not + interchangeable. Preserve invocation, streaming, completion, cancellation, ping, close, ack, and + sequence semantics in every affected protocol. +- Preserve forward-compatible parser behavior. In particular, do not reject currently tolerated + unknown JSON properties, unknown message types, or trailing MessagePack data without a deliberate + protocol compatibility plan. +- Text framing uses the record separator. Binary framing uses a length prefix and must continue to + handle segmented input, incomplete messages, multiple buffered messages, and invalid lengths. +- Defaults are observable contracts. Changes to serializers, protocol versions, transfer formats, + timeouts, keep-alive behavior, reconnect, headers, or error details require compatibility analysis + across the server and every supported client. + +Protocol-only changes belong with focused tests under `common/SignalR.Common/test`. Transport +behavior belongs with `common/Http.Connections/test`; do not use a hub integration test as the only +proof of framing or transport behavior. + +## Connection and hub lifetime + +`HubConnectionHandler` coordinates the server lifetime around the hub dispatcher and +`HubLifetimeManager`. Preserve the ordering and cleanup relationships among connection +initialization, `OnConnectedAsync`, hub dispatch, connection cleanup, and `OnDisconnectedAsync`. + +- Normal close, abort, timeout, handshake failure, and application exceptions are distinct paths. + Validate cleanup and observable errors for every path affected by a change. +- Connection-aborted tokens, active invocations, upload streams, message buffers, groups, users, and + provider state must not outlive the connection that owns them. +- Keep per-connection state on `HubConnectionContext` or its features instead of passing parallel + values that can drift between the HTTP connection, hub dispatcher, and lifetime manager. +- Hub invocations are serialized per connection by default. + `HubOptions.MaximumParallelInvocationsPerClient` changes the non-streaming invocation limit; + streaming invocations are intentionally not counted by that limit. +- Propagate cancellation through dispatch, stream reads and writes, and user handlers. Complete + stream trackers, linked cancellation sources, scopes, activated hubs, and owned filters on success, + cancellation, and failure. +- Do not replace awaitable lifetime work with blocking waits or unobserved tasks. Any intentionally + detached operation needs an owner, cancellation, and an observable failure path. + +Use `HubConnectionHandlerTests`, `HubFilterTests`, and the dispatcher tests under +`server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests` for server lifetime and dispatch behavior. + +## Hubs, options, DI, and extensibility + +- Register SignalR and map hubs through the integration layer in `server/SignalR`; keep reusable hub + and lifetime abstractions in `server/Core`. +- Global `HubOptions` are copied into per-hub `HubOptions` before per-hub configuration is + applied. Preserve inherited values, validation, and user configuration order. +- Hub filters may be supplied as instances or activated through DI. Preserve the distinction between + container-owned and framework-created instances and dispose only instances the framework owns. +- Keep typed hub clients and strongly typed proxies aligned with dynamic hub scenarios. Do not expose + internal dispatch or reflection plumbing solely to avoid maintaining the intended public contract. +- Public or protected API changes must follow the + [API review process](APIReviewProcess.md) and update the applicable + [API baseline](APIBaselines.md). Public API baselines record compatibility; they do not replace API + design review. +- Hub discovery, method binding, typed proxies, and serializers are trimming- and AOT-sensitive. + Preserve existing annotations and validate publish-time behavior using the + [trimming guidance](Trimming.md) when reflection or generated metadata changes. + +## Transports, negotiation, and reconnect + +The transports share a connection abstraction but do not have identical HTTP or framing behavior: + +- WebSockets is full duplex and can carry text or binary frames. It is the only transport for which + clients may skip negotiation. +- Server-Sent Events is server-to-client and text-only; client-to-server data uses HTTP POST. Preserve + SignalR's supported newline normalization rather than assuming arbitrary event-stream framing. +- Long Polling is server-to-client and pairs with HTTP POST. Its `200`, `204`, and error responses + distinguish poll timeout, connection completion, and failure. +- Negotiation selects from server-advertised transports and transfer formats. Preserve + `negotiateVersion`, the distinction between public connection ID and secret connection token, + redirect/error payloads, and explicit client transport selection. +- Stateful reconnect is opt-in at both ends and uses the ack/sequence protocol and message buffer. + Do not treat it as ordinary automatic reconnect or assume that every client or transport supports + it. Preserve buffer limits, cancellation under backpressure, duplicate suppression, and resend + ordering. +- Browser WebSockets and Server-Sent Events cannot set arbitrary request headers. Keep documented + access-token alternatives and do not generalize that restriction to Long Polling or non-browser + clients. + +Exercise transport selection and negotiation in `HttpConnectionDispatcherTests` and +`NegotiateProtocolTests`. Exercise WebSocket, Server-Sent Events, and Long Polling details in their +transport-specific tests. + +## Client compatibility + +The clients implement the same Hub Protocol but do not have feature parity. Check the affected +client explicitly instead of inferring behavior from another implementation. + +| Capability | .NET client | TypeScript client | Java client | +| --- | --- | --- | --- | +| Transports | WebSockets, Server-Sent Events, Long Polling | WebSockets, Server-Sent Events, Long Polling | WebSockets and Long Polling | +| Skip negotiation | WebSockets only | WebSockets only | WebSockets only | +| Automatic reconnect | Opt-in with `WithAutomaticReconnect` | Opt-in with `withAutomaticReconnect` | Not implemented | +| Stateful reconnect | Separate opt-in; WebSockets only | Separate stateful reconnect support | Not implemented | +| Streaming surface | `IAsyncEnumerable` and `ChannelReader` with cancellation | `IStreamResult` and disposable subscriptions | RxJava `Observable` and `Disposable.dispose()` for stream cancellation | + +Header, cookie, proxy, certificate, credential, and WebSocket configuration support also varies by +platform. Preserve the guards and alternatives on browser-sensitive .NET APIs and the +transport-specific behavior in the TypeScript client. + +Client callbacks and subscriptions need deterministic removal. Preserve pending invocation +completion, cancellation, and terminal errors across stop and reconnect paths using the conventions +of that client rather than introducing a new cross-client abstraction. In the Java client, +`Subscription.unsubscribe()` removes hub-method handlers, while RxJava `Disposable.dispose()` +cancels a stream subscription. + +## Scaleout, groups, and users + +`DefaultHubLifetimeManager` owns in-process routing. `RedisHubLifetimeManager` adds +cross-server channels for hub, group, user, and connection messages. + +- Preserve local-delivery short-circuiting and the routing identity required by each message type. +- Remote group add/remove operations use a management channel and acknowledgements; keep connection + cleanup and membership changes consistent across servers. +- Do not assume that scaleout gives stronger replay or ordering guarantees than the provider and + tests establish. +- Add single-server lifetime-manager contracts to `HubLifetimeManagerTestsBase`, reusable + cross-server contracts to `ScaleoutHubLifetimeManagerTests`, and Redis-specific + behavior to the StackExchange.Redis tests. +- Isolate group names, user names, connections, and provider state in parallel tests. + +## Testing and validation + +Choose the smallest test boundary that owns the changed behavior: + +| Change | Primary test boundary | +| --- | --- | +| Framing, handshake, or hub-message parsing | `common/SignalR.Common/test` | +| Negotiation or a server transport | `common/Http.Connections/test` | +| Hub dispatch, lifetime, options, filters, or reconnect integration | `server/SignalR/test` | +| Lifetime-manager contract | `server/Specification.Tests` | +| Provider-independent cross-server behavior | `server/Specification.Tests` | +| Redis-specific routing or integration | `server/StackExchangeRedis/test` | +| .NET client behavior | `clients/csharp/**/test` | +| TypeScript client behavior | `clients/ts/**/tests`; use `FunctionalTests` for hosted browser/client-server behavior | +| Java client behavior | `clients/java/signalr/test` | +| Performance-sensitive protocol or dispatch work | `perf/Microbenchmarks` or the existing Crankier load application | + +Follow the [repository test requirements](../CONTRIBUTING.md#tests) and +[faithful-validation requirements](../.github/copilot-instructions.md#running-tests). The +[SignalR README](../src/SignalR/README.md#test) describes the area build and test entry points, and +the TypeScript-specific workflows are documented in +[JS unit tests](../src/SignalR/docs/JSUnitTests.md) and +[JS functional tests](../src/SignalR/docs/JSFunctionalTests.md). + +Tests should assert observable message shape, transport status or close behavior, lifetime +transitions, completion, cancellation, ordering, diagnostics, and cleanup. Avoid timing-only waits; +use bounded coordination that makes hangs and shutdown races fail deterministically. From 7826960fa166e104ffae805ec395344d35f4872c Mon Sep 17 00:00:00 2001 From: PureWeen <223556219+Copilot@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:01:08 -0500 Subject: [PATCH 2/6] Align SignalR guidance with architecture pattern Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/instructions/signalr.instructions.md | 7 - docs/README.md | 1 - docs/SignalRGuidance.md | 185 ------------- src/SignalR/ARCHITECTURE.md | 275 +++++++++++++++++++ 4 files changed, 275 insertions(+), 193 deletions(-) delete mode 100644 .github/instructions/signalr.instructions.md delete mode 100644 docs/SignalRGuidance.md create mode 100644 src/SignalR/ARCHITECTURE.md diff --git a/.github/instructions/signalr.instructions.md b/.github/instructions/signalr.instructions.md deleted file mode 100644 index a3c55634f9cf..000000000000 --- a/.github/instructions/signalr.instructions.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -description: Instructions for folder 'src\SignalR' -applyTo: "src/SignalR/**" ---- - -For SignalR work, follow the relevant sections of the -[SignalR contributor guidance](../../docs/SignalRGuidance.md). diff --git a/docs/README.md b/docs/README.md index 089d3efa0c25..05b50f3f8afa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,4 +30,3 @@ The table below outlines the different docs in this folder and what they are hel | [Adding new Projects to the Repo](AddingNewProjects.md) | Outlines the process of adding new projects (i.e. `.csproj` files) to the repo | Anyone who finds themselves trying to add a new project and including it in the build. | | [Using WebTransport in Kestrel](WebTransport.md) | Outlines how to setup Kestrel to use WebTransport | Anyone looking to support WebTransport | | [Benchmarking](Benchmarks.md) | Instructions on how to benchmark PRs and local changes | .NET team | -| [SignalR contributor guidance](SignalRGuidance.md) | Implementation, design, testing, and review guidance for SignalR | Contributors working under `src/SignalR/**` | diff --git a/docs/SignalRGuidance.md b/docs/SignalRGuidance.md deleted file mode 100644 index eb6fe0eede0b..000000000000 --- a/docs/SignalRGuidance.md +++ /dev/null @@ -1,185 +0,0 @@ -# SignalR contributor guidance - -This guidance covers implementation, design, testing, and review work under `src/SignalR/**`. -Use it with the repository-wide contributor instructions and the documentation already maintained -inside the SignalR tree. - -## Source ownership - -Keep changes in the layer that owns the behavior: - -| Area | Responsibility | -| --- | --- | -| [`server/Core`](../src/SignalR/server/Core) | Hub abstractions, dispatch, connection lifetime, groups, users, filters, and the in-process `HubLifetimeManager`. | -| [`server/SignalR`](../src/SignalR/server/SignalR) | ASP.NET Core DI and endpoint integration, plus server integration tests. | -| [`server/StackExchangeRedis`](../src/SignalR/server/StackExchangeRedis) | Redis scaleout and its cross-server routing tests. | -| [`server/Specification.Tests`](../src/SignalR/server/Specification.Tests) | Reusable conformance tests for `HubLifetimeManager` implementations. | -| [`common/Http.Connections`](../src/SignalR/common/Http.Connections) | Negotiation and the server implementations of WebSockets, Server-Sent Events, Long Polling, and HTTP sends. | -| [`common/SignalR.Common`](../src/SignalR/common/SignalR.Common) | Shared hub messages, handshake support, and related test infrastructure. | -| [`common/Shared`](../src/SignalR/common/Shared) | Shared text and binary framing plus stateful reconnect buffering compiled into multiple projects. | -| [`common/Protocols.*`](../src/SignalR/common) | JSON, MessagePack, and Newtonsoft.Json hub protocol implementations. | -| [`clients`](../src/SignalR/clients) | Independent .NET, TypeScript, and Java clients with different platform and feature capabilities. | -| [`docs/specs`](../src/SignalR/docs/specs) | The Hub Protocol and transport wire specifications. | - -Do not move HTTP transport behavior into hub dispatch, provider-specific scaleout behavior into the -core lifetime manager, or client-specific constraints into shared protocol contracts. Changes that -cross these boundaries should preserve the abstractions between them and include tests at each -affected boundary. - -## Wire contracts and compatibility - -Treat the [Hub Protocol](../src/SignalR/docs/specs/HubProtocol.md) and -[transport protocols](../src/SignalR/docs/specs/TransportProtocols.md) as compatibility contracts. - -- The Hub Protocol assumes reliable, ordered message delivery and does not provide general - retransmission or reordering. -- The client handshake is the first hub-protocol message. It is JSON regardless of the selected hub - protocol and uses the record-separator text framing implemented by `HandshakeProtocol`. -- JSON and MessagePack represent the same logical message families, but their wire encodings are not - interchangeable. Preserve invocation, streaming, completion, cancellation, ping, close, ack, and - sequence semantics in every affected protocol. -- Preserve forward-compatible parser behavior. In particular, do not reject currently tolerated - unknown JSON properties, unknown message types, or trailing MessagePack data without a deliberate - protocol compatibility plan. -- Text framing uses the record separator. Binary framing uses a length prefix and must continue to - handle segmented input, incomplete messages, multiple buffered messages, and invalid lengths. -- Defaults are observable contracts. Changes to serializers, protocol versions, transfer formats, - timeouts, keep-alive behavior, reconnect, headers, or error details require compatibility analysis - across the server and every supported client. - -Protocol-only changes belong with focused tests under `common/SignalR.Common/test`. Transport -behavior belongs with `common/Http.Connections/test`; do not use a hub integration test as the only -proof of framing or transport behavior. - -## Connection and hub lifetime - -`HubConnectionHandler` coordinates the server lifetime around the hub dispatcher and -`HubLifetimeManager`. Preserve the ordering and cleanup relationships among connection -initialization, `OnConnectedAsync`, hub dispatch, connection cleanup, and `OnDisconnectedAsync`. - -- Normal close, abort, timeout, handshake failure, and application exceptions are distinct paths. - Validate cleanup and observable errors for every path affected by a change. -- Connection-aborted tokens, active invocations, upload streams, message buffers, groups, users, and - provider state must not outlive the connection that owns them. -- Keep per-connection state on `HubConnectionContext` or its features instead of passing parallel - values that can drift between the HTTP connection, hub dispatcher, and lifetime manager. -- Hub invocations are serialized per connection by default. - `HubOptions.MaximumParallelInvocationsPerClient` changes the non-streaming invocation limit; - streaming invocations are intentionally not counted by that limit. -- Propagate cancellation through dispatch, stream reads and writes, and user handlers. Complete - stream trackers, linked cancellation sources, scopes, activated hubs, and owned filters on success, - cancellation, and failure. -- Do not replace awaitable lifetime work with blocking waits or unobserved tasks. Any intentionally - detached operation needs an owner, cancellation, and an observable failure path. - -Use `HubConnectionHandlerTests`, `HubFilterTests`, and the dispatcher tests under -`server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests` for server lifetime and dispatch behavior. - -## Hubs, options, DI, and extensibility - -- Register SignalR and map hubs through the integration layer in `server/SignalR`; keep reusable hub - and lifetime abstractions in `server/Core`. -- Global `HubOptions` are copied into per-hub `HubOptions` before per-hub configuration is - applied. Preserve inherited values, validation, and user configuration order. -- Hub filters may be supplied as instances or activated through DI. Preserve the distinction between - container-owned and framework-created instances and dispose only instances the framework owns. -- Keep typed hub clients and strongly typed proxies aligned with dynamic hub scenarios. Do not expose - internal dispatch or reflection plumbing solely to avoid maintaining the intended public contract. -- Public or protected API changes must follow the - [API review process](APIReviewProcess.md) and update the applicable - [API baseline](APIBaselines.md). Public API baselines record compatibility; they do not replace API - design review. -- Hub discovery, method binding, typed proxies, and serializers are trimming- and AOT-sensitive. - Preserve existing annotations and validate publish-time behavior using the - [trimming guidance](Trimming.md) when reflection or generated metadata changes. - -## Transports, negotiation, and reconnect - -The transports share a connection abstraction but do not have identical HTTP or framing behavior: - -- WebSockets is full duplex and can carry text or binary frames. It is the only transport for which - clients may skip negotiation. -- Server-Sent Events is server-to-client and text-only; client-to-server data uses HTTP POST. Preserve - SignalR's supported newline normalization rather than assuming arbitrary event-stream framing. -- Long Polling is server-to-client and pairs with HTTP POST. Its `200`, `204`, and error responses - distinguish poll timeout, connection completion, and failure. -- Negotiation selects from server-advertised transports and transfer formats. Preserve - `negotiateVersion`, the distinction between public connection ID and secret connection token, - redirect/error payloads, and explicit client transport selection. -- Stateful reconnect is opt-in at both ends and uses the ack/sequence protocol and message buffer. - Do not treat it as ordinary automatic reconnect or assume that every client or transport supports - it. Preserve buffer limits, cancellation under backpressure, duplicate suppression, and resend - ordering. -- Browser WebSockets and Server-Sent Events cannot set arbitrary request headers. Keep documented - access-token alternatives and do not generalize that restriction to Long Polling or non-browser - clients. - -Exercise transport selection and negotiation in `HttpConnectionDispatcherTests` and -`NegotiateProtocolTests`. Exercise WebSocket, Server-Sent Events, and Long Polling details in their -transport-specific tests. - -## Client compatibility - -The clients implement the same Hub Protocol but do not have feature parity. Check the affected -client explicitly instead of inferring behavior from another implementation. - -| Capability | .NET client | TypeScript client | Java client | -| --- | --- | --- | --- | -| Transports | WebSockets, Server-Sent Events, Long Polling | WebSockets, Server-Sent Events, Long Polling | WebSockets and Long Polling | -| Skip negotiation | WebSockets only | WebSockets only | WebSockets only | -| Automatic reconnect | Opt-in with `WithAutomaticReconnect` | Opt-in with `withAutomaticReconnect` | Not implemented | -| Stateful reconnect | Separate opt-in; WebSockets only | Separate stateful reconnect support | Not implemented | -| Streaming surface | `IAsyncEnumerable` and `ChannelReader` with cancellation | `IStreamResult` and disposable subscriptions | RxJava `Observable` and `Disposable.dispose()` for stream cancellation | - -Header, cookie, proxy, certificate, credential, and WebSocket configuration support also varies by -platform. Preserve the guards and alternatives on browser-sensitive .NET APIs and the -transport-specific behavior in the TypeScript client. - -Client callbacks and subscriptions need deterministic removal. Preserve pending invocation -completion, cancellation, and terminal errors across stop and reconnect paths using the conventions -of that client rather than introducing a new cross-client abstraction. In the Java client, -`Subscription.unsubscribe()` removes hub-method handlers, while RxJava `Disposable.dispose()` -cancels a stream subscription. - -## Scaleout, groups, and users - -`DefaultHubLifetimeManager` owns in-process routing. `RedisHubLifetimeManager` adds -cross-server channels for hub, group, user, and connection messages. - -- Preserve local-delivery short-circuiting and the routing identity required by each message type. -- Remote group add/remove operations use a management channel and acknowledgements; keep connection - cleanup and membership changes consistent across servers. -- Do not assume that scaleout gives stronger replay or ordering guarantees than the provider and - tests establish. -- Add single-server lifetime-manager contracts to `HubLifetimeManagerTestsBase`, reusable - cross-server contracts to `ScaleoutHubLifetimeManagerTests`, and Redis-specific - behavior to the StackExchange.Redis tests. -- Isolate group names, user names, connections, and provider state in parallel tests. - -## Testing and validation - -Choose the smallest test boundary that owns the changed behavior: - -| Change | Primary test boundary | -| --- | --- | -| Framing, handshake, or hub-message parsing | `common/SignalR.Common/test` | -| Negotiation or a server transport | `common/Http.Connections/test` | -| Hub dispatch, lifetime, options, filters, or reconnect integration | `server/SignalR/test` | -| Lifetime-manager contract | `server/Specification.Tests` | -| Provider-independent cross-server behavior | `server/Specification.Tests` | -| Redis-specific routing or integration | `server/StackExchangeRedis/test` | -| .NET client behavior | `clients/csharp/**/test` | -| TypeScript client behavior | `clients/ts/**/tests`; use `FunctionalTests` for hosted browser/client-server behavior | -| Java client behavior | `clients/java/signalr/test` | -| Performance-sensitive protocol or dispatch work | `perf/Microbenchmarks` or the existing Crankier load application | - -Follow the [repository test requirements](../CONTRIBUTING.md#tests) and -[faithful-validation requirements](../.github/copilot-instructions.md#running-tests). The -[SignalR README](../src/SignalR/README.md#test) describes the area build and test entry points, and -the TypeScript-specific workflows are documented in -[JS unit tests](../src/SignalR/docs/JSUnitTests.md) and -[JS functional tests](../src/SignalR/docs/JSFunctionalTests.md). - -Tests should assert observable message shape, transport status or close behavior, lifetime -transitions, completion, cancellation, ordering, diagnostics, and cleanup. Avoid timing-only waits; -use bounded coordination that makes hangs and shutdown races fail deterministically. diff --git a/src/SignalR/ARCHITECTURE.md b/src/SignalR/ARCHITECTURE.md new file mode 100644 index 000000000000..f48a9a527ea9 --- /dev/null +++ b/src/SignalR/ARCHITECTURE.md @@ -0,0 +1,275 @@ +# ASP.NET Core SignalR Architecture + +## Purpose and Scope + +This document describes how the subsystems under `src/SignalR` compose to provide bidirectional communication between ASP.NET Core applications and independently implemented clients. It explains the responsibilities, state ownership, wire contracts, and failure boundaries of the hub, connection, transport, protocol, and scaleout layers. + +The intended audience is contributors who need to understand where behavior belongs and how a change in one layer affects the others. This is the authoritative architecture overview for the SignalR area. It describes runtime relationships rather than cataloging every project or feature. + +This document is not an API reference, an exhaustive inventory of projects and source files, or a build and test workflow. The [SignalR README](README.md) provides the area introduction, product documentation links, and development entry points. The [Hub Protocol](docs/specs/HubProtocol.md) and [Transport Protocols](docs/specs/TransportProtocols.md) describe the wire contracts in detail. + +## System Overview + +SignalR separates the application's hub programming model from the mechanism that carries messages. A hub exposes application methods to connected clients and can address clients by connection, group, or user. Client and server protocol implementations translate invocations, results, streams, and connection-control messages into a common wire contract. Transports carry the encoded data without interpreting hub methods or application arguments. + +On the server, ASP.NET Core endpoint integration composes the HTTP connection layer with the hub connection handler. The connection layer presents a duplex connection even when sending and receiving use separate HTTP requests. Above that boundary, the hub runtime performs the handshake, reads and writes hub messages, dispatches application calls, and coordinates connection shutdown. + +The hub lifetime manager is a separate routing boundary. The default implementation reaches connections in the current server process; a scaleout implementation extends addressing across servers. Replacing that service changes delivery topology, not the hub programming model or the client-facing protocol. + +### Composition Diagram + +Solid arrows represent service composition and use. Dashed arrows represent runtime communication. This is a responsibility map, not an exhaustive assembly-dependency graph. + +```mermaid +flowchart TB + Client["Independent .NET, TypeScript, or Java client
Hub connection, protocol, and transports"] + + subgraph Server["ASP.NET Core server"] + Integration["Endpoint and DI integration"] + HTTP["HTTP connection layer
Negotiation and transports"] + HubConnection["Hub connection handler and context"] + Protocol["Hub protocol implementation"] + Dispatcher["Hub dispatcher"] + Application["Application hubs and filters"] + Lifetime["Hub lifetime manager"] + Local["In-process routing"] + Redis["Redis routing"] + + Integration --> HTTP + Integration --> HubConnection + HubConnection --> Protocol + HubConnection --> Dispatcher + Dispatcher --> Application + HubConnection --> Lifetime + Dispatcher --> Lifetime + Lifetime --> Local + Lifetime --> Redis + HTTP <-. "duplex connection" .-> HubConnection + end + + Client <-. "HTTP and transport data" .-> HTTP + Redis <-. "backplane messages" .-> Backplane["Redis and other server instances"] +``` + +In-process routing and Redis routing are alternative lifetime-manager implementations. Each server still owns its local connections, their selected protocols, and their application execution. Outbound messages, including messages routed through the backplane, ultimately pass through those local connection contexts. + +## Architectural Layers and Ownership + +### Endpoint and HTTP Connection Integration + +[`server/SignalR`](server/SignalR) owns the ASP.NET Core integration used to register SignalR services and map hubs to endpoints. It joins the HTTP connection pipeline to the hub runtime; it does not implement a second dispatcher or transport stack. + +[`common/Http.Connections`](common/Http.Connections) owns server-side negotiation, HTTP connection management, WebSockets, Server-Sent Events, Long Polling, and HTTP sends. It correlates transport requests with a connection and exposes connection features and duplex pipes to the application above it. This layer can host connection-oriented applications without understanding hub invocations. + +[`common/Http.Connections.Common`](common/Http.Connections.Common) holds HTTP connection contracts shared by the server and .NET client, including negotiation support and transport kinds. HTTP-specific behavior remains below the hub boundary rather than becoming policy in hub dispatch. + +### Hub Runtime + +[`server/Core`](server/Core) owns hub abstractions, dispatch, connection lifetime, filters, user and group addressing, typed client proxies, and the lifetime-manager abstraction and default implementation. + +The main runtime roles are distinct: + +- `HubConnectionHandler` coordinates one hub connection: handshake, lifetime-manager notifications, dispatcher callbacks, the message loop, and shutdown. +- `HubConnectionContext` carries connection-owned state and coordinates writes, cancellation, timeouts, and optional reconnect buffering. +- `DefaultHubDispatcher` discovers hub methods, supplies binding information, authorizes and dispatches invocations, activates hubs and filters, and coordinates invocation and stream completion. +- `HubLifetimeManager` provides client routing and group operations and receives connection-lifetime notifications. It is not the owner of application method execution. + +Application hub instances are short-lived activations, not the long-lived connection itself. The dispatcher initializes them with the connection's caller context, client access, and group manager. `IHubContext` provides client addressing outside a hub invocation without retaining a hub instance. + +### Shared Messages, Framing, and Encodings + +[`common/SignalR.Common`](common/SignalR.Common) owns the shared .NET hub-message model, protocol abstractions, and handshake support. [`common/Shared`](common/Shared) contains implementation source, including text and binary framing and reconnect buffering, that is compiled into multiple projects. Shared source does not imply a single shared runtime instance. + +The `common/Protocols.*` projects implement the encoding boundary: + +- [`Protocols.Json`](common/Protocols.Json) uses System.Text.Json for JSON hub messages. +- [`Protocols.NewtonsoftJson`](common/Protocols.NewtonsoftJson) provides the JSON encoding using Newtonsoft.Json. +- [`Protocols.MessagePack`](common/Protocols.MessagePack) provides the binary MessagePack encoding. + +These implementations share logical message semantics, but serializers still determine how application arguments and results map to wire values. The TypeScript and Java clients have their own protocol implementations; the .NET shared libraries are not an implementation dependency for those clients. + +### Scaleout and Conformance Infrastructure + +[`server/StackExchangeRedis`](server/StackExchangeRedis) owns the Redis lifetime manager and backplane protocol. Provider-specific channels, subscriptions, management acknowledgements, and failure handling remain behind the lifetime-manager boundary. + +[`server/Specification.Tests`](server/Specification.Tests) expresses reusable lifetime-manager contracts, including cross-server behavior. It is conformance infrastructure, not a runtime layer. Its contracts distinguish behavior that every applicable lifetime manager must provide from behavior specific to Redis. + +### Independent Clients + +[`clients/csharp`](clients/csharp), [`clients/ts`](clients/ts), and [`clients/java`](clients/java) own separate connection state machines, HTTP and transport adaptation, handler registration, pending invocations, and streaming surfaces. They interoperate through the wire protocols, not through a common client implementation. Their feature and platform boundaries are described in [Independent Client Implementations](#independent-client-implementations). + +## Connection Establishment and Hub Handshake + +Connection establishment has two different agreements. HTTP negotiation establishes how the connection will be transported. The hub handshake establishes how messages on that connection will be interpreted. Success at the first boundary does not imply success at the second. + +In the negotiated path, the server advertises available transports and their transfer formats. The client selects a compatible transport using that advertisement, its explicit configuration, and the capabilities of its environment. Negotiation also supplies the identity used to correlate later transport requests. In negotiation version 1, the public connection ID and the secret connection token have different purposes: the ID is used for application addressing, while the token associates later HTTP requests with the connection and must remain secret. They are not interchangeable. + +A client configured to use WebSockets directly can skip HTTP negotiation. WebSockets is the only transport supporting this path. Skipping negotiation does not skip the hub handshake, and it is not a way to enable features that require a negotiated agreement, such as stateful reconnect. + +Once the transport connection is open, the client's first hub-level message is a handshake request naming a hub protocol and version. The request and response are always JSON with record-separator text framing, including when subsequent hub messages use MessagePack. The server resolves the requested protocol, checks that it is supported and compatible with the transport's transfer format, and responds before normal hub dispatch begins. + +The selected hub protocol belongs to that connection for its lifetime. Negotiation protocol versions and hub protocol versions govern separate contracts. A handshake rejection or timeout ends startup before the connection enters normal hub lifetime processing; opening an HTTP transport alone does not create a successfully connected hub client. + +## Transport and Protocol Separation + +### Transport Composition and Transfer Formats + +The HTTP transport layer supplies bidirectional communication through different compositions: + +| Transport | Receive and send composition | Transfer formats | +| --- | --- | --- | +| WebSockets | A single full-duplex transport carries data in both directions. | Text and binary | +| Server-Sent Events | The event stream carries server-to-client data; separate HTTP POST requests carry client-to-server data. | Text | +| Long Polling | Repeated polls carry server-to-client data; separate HTTP POST requests carry client-to-server data. | Text and binary | + +Server-Sent Events and Long Polling are receive-side half-transports combined with HTTP sends to form the duplex abstraction. An individual HTTP request completing is therefore not equivalent to the hub connection ending. Poll timeout, connection completion, and transport failure are distinct outcomes owned by the HTTP connection layer. + +Transfer format constrains which hub encoding can use a transport. JSON uses text and MessagePack uses binary; transport selection must satisfy the chosen protocol rather than silently changing its encoding. Server-Sent Events also has text line-ending normalization behavior, so it is not a substitute for byte-preserving binary delivery. + +### Message Semantics and Framing + +The Hub Protocol assumes reliable, ordered delivery from the underlying connection. It does not generally reorder messages or retransmit arbitrary lost traffic. The opt-in stateful reconnect extension provides a narrower resumption mechanism described below, not a replacement for that transport requirement. + +JSON and MessagePack express the same logical message families: invocation, streamed items, completion, cancellation, ping, close, acknowledgement, and sequence. The encoding does not change the meaning of an invocation ID or turn an invocation error into a connection error. It does change the representation of fields and application values, so semantic parity does not imply interchangeable bytes or identical serializer configuration. + +Text messages end with the record separator. Binary messages have a length prefix. Transport reads and hub-message boundaries need not align: a read may contain part of one message or several complete messages. Framing owns identifying complete messages from segmented input and rejecting invalid lengths; protocol parsing owns interpreting the framed payload. + +Forward-compatible parsing is part of the encoding contract. The .NET protocol parsers tolerate unknown JSON properties and unknown hub-message types, and their MessagePack parser permits additional trailing array elements. These tolerances must not be assumed across all client implementations. This extensibility is different from accepting malformed required fields or invalid framing. Tightening parsing can break an older implementation's ability to communicate with a newer peer even when the messages it understands have not changed. + +## Server Dispatch and Lifetime + +### Connection and Invocation State + +After a successful handshake, the handler notifies the lifetime manager that the connection exists and enters hub processing. The dispatcher invokes the application's connected callback before normal message dispatch. During normal shutdown, the handler coordinates hub disconnection and then performs connection cleanup and the lifetime manager's disconnected notification. Startup failures take shorter paths and do not imply that every application lifetime callback has run. + +`HubConnectionContext` and the underlying connection features are the source of truth for per-connection state. This includes the caller identity, selected protocol, abort signal, active requests, upload-stream tracking, write coordination, and reconnect state. Lifetime-manager group membership and provider subscriptions refer to that connection and are removed when it ends. Keeping these relationships attached to their owner avoids parallel copies of state drifting between transport, dispatcher, and routing layers. + +Each hub invocation has its own activation and service scope. A streaming invocation retains the resources needed to produce or consume its stream until that operation finishes; returning control to the message loop does not release ownership. Hubs, invocation scopes, cancellation sources, stream trackers, and framework-created filters all have cleanup paths independent of whether application execution succeeds. + +### Concurrency and Streaming + +Ordinary non-streaming hub invocations are serialized per connection by default. `HubOptions.MaximumParallelInvocationsPerClient` changes that invocation limit; it does not impose a single global lock on the server or make hub instances connection-scoped. + +Invocations with streaming parameters or streaming results do not consume this non-streaming invocation limit. The dispatcher can continue processing messages while a stream is active, which is necessary to receive upload items, cancellation, and other connection traffic. Stream ownership and completion therefore cannot be inferred from the completion of the dispatch call that started the stream. + +Cancellation crosses both invocation and connection boundaries. Invocation cancellation targets the corresponding active operation; connection abortion signals that the connection can no longer support its work. Stream completion, linked cancellation, hub release, and scope disposal remain owned by the invocation and connection machinery, including when application code observes cancellation asynchronously. + +### Close, Abort, Timeout, and Error Boundaries + +These outcomes are related but not interchangeable: + +- A hub method failure normally completes that invocation with an error, when a response is expected, rather than terminating unrelated work on the connection. +- A hub `Close` message communicates a connection-level outcome, including error and reconnect information. It is not an HTTP status or a WebSocket close frame. +- Abort terminates connection use and signals cancellation. In the normal hub disconnection path, the handler attempts the hub close message and waits for abort callbacks before invoking the application's disconnected callback. +- Handshake timeout belongs to startup. Client timeout belongs to liveness detection on an established connection. Keep-alive traffic and application traffic feed that liveness mechanism. +- Protocol errors, transport failures, and exceptions in hub lifetime callbacks have different entry points and may prevent later phases from running. + +The error exposed to the peer and the exception observed by application lifetime callbacks are deliberate translations, not necessarily the same exception. Detailed error options affect disclosure. Cleanup and diagnostics must still distinguish a graceful close, an invocation failure, an abandoned startup, and an unexpectedly lost connection. + +## Configuration, Extensibility, and Reflection + +Global `HubOptions` establish the configuration inherited by individual hubs. Per-hub `HubOptions` configuration is applied on top of the global setup; collections such as supported protocols and filters are copied rather than sharing a mutable list across hubs. The handler resolves effective settings before creating connection state. This keeps timeout, concurrency, protocol, and buffering decisions consistent within the connection that uses them. + +Hub filters wrap invocation and lifetime callbacks without taking ownership of the connection. A filter can be supplied as an instance, resolved from DI, or created by the framework when its type is not registered. These choices imply different disposal owners. The filter factory releases instances it creates; a DI-resolved filter follows its container's lifetime, and a supplied instance is not made framework-owned merely by participating in the pipeline. + +Hub method discovery, argument binding, typed client proxy generation, and application serialization form a reflection-sensitive boundary. Method metadata is discovered and reused by the dispatcher, while invocation data and activated services remain local to each call. Trimming annotations preserve metadata needed by discovery and binding, but metadata preservation is not the same as runtime code-generation support. For example, the typed client builder generates proxy code and has a dynamic-code requirement. A working reflection-based path does not by itself establish Native AOT support for every hub or serializer configuration. + +## Stateful Reconnect + +Stateful reconnect preserves an existing logical connection across a temporary transport interruption. It is opt-in at both the server endpoint and a supporting client, requires negotiation, and currently uses WebSockets. It does not apply to every transport or client implementation. + +The transport layer owns reconnecting the transport to retained connection state. The hub layer owns the message continuity needed above that replacement: + +- Outgoing invocation-related messages are retained in a connection-owned message buffer until acknowledged. +- `Ack` messages advance the acknowledged position and release buffered data. +- `Sequence` messages establish the sending position when resuming; the receiver uses sequence tracking to suppress duplicates. +- Resend coordination orders retained messages with subsequent writes. Buffer limits introduce backpressure, so acknowledgement progress, cancellation, and disposal are part of the connection's resource lifetime. + +The shared .NET implementation is in [`common/Shared/MessageBuffer.cs`](common/Shared/MessageBuffer.cs); the TypeScript client implements its own corresponding state machine. Acknowledgement means receipt at the protocol boundary, not that an application operation has committed a durable side effect. + +Ordinary automatic reconnect is a different client policy: it attempts to establish a new connection after the previous one is lost. It does not retain the old server connection, group memberships, or an unacknowledged-message buffer. Stateful reconnect instead depends on the original server connection still existing and the resumption succeeding. It is not durable storage, process failover, or a general exactly-once delivery guarantee, and a Redis backplane does not turn it into those things. + +## Scaleout, Groups, and Users + +`DefaultHubLifetimeManager` tracks and addresses connections in the current process. Broadcasts, group sends, user sends, and connection-targeted sends resolve to local connection contexts. Group membership is associated with connections, while user addressing uses the user identifier supplied for those connections; a user may have multiple connections. + +`RedisHubLifetimeManager` keeps local connection ownership but extends routing using Redis channels for hub-wide, group, user, and connection messages. Messages received from Redis are delivered through the local connection contexts and their selected hub protocols. Backplane serialization is an internal server-to-server contract, distinct from the hub encoding selected by each client. + +Some operations can be completed locally without a Redis round trip, such as a send to a connection owned by the current server. A group add or remove targeting another server instead uses a management channel and an acknowledgement from the owner. These management acknowledgements confirm routing-state operations; they are unrelated to the hub `Ack` messages used for stateful reconnect. + +Disconnect cleanup removes local connections and their group and provider subscription state. Scaleout does not move hub instances, invocation scopes, or live connection buffers to Redis. It also does not establish durable replay or a total order across all senders beyond the guarantees of the provider and the operations involved. + +`HubLifetimeManagerTestsBase` defines reusable single-server contracts. `ScaleoutHubLifetimeManagerTests` extends that boundary to multiple server instances. Redis-specific tests cover the provider's channel, connection, and routing behavior rather than redefining the common lifetime-manager contract. + +## Independent Client Implementations + +The clients share wire-level meaning, but their connection state machines, platform adapters, and application surfaces are independent. A server or .NET client implementation detail is not automatically a capability of the TypeScript or Java client. + +| Capability | .NET client | TypeScript client | Java client | +| --- | --- | --- | --- | +| HTTP transports | WebSockets, Server-Sent Events, Long Polling | WebSockets, Server-Sent Events, Long Polling | WebSockets and Long Polling; no Server-Sent Events | +| Skip negotiation | WebSockets only | WebSockets only | WebSockets only | +| Automatic reconnect | Opt-in with `WithAutomaticReconnect` | Opt-in with `withAutomaticReconnect` | Not implemented | +| Stateful reconnect | Separate opt-in; WebSockets only | Separate opt-in; WebSockets only | Not implemented | +| Streaming surface | `IAsyncEnumerable` and `ChannelReader`, with cancellation | `IStreamResult` and disposable subscriptions | RxJava `Observable` and `Disposable.dispose()` | + +The .NET client separates its hub connection in `Client.Core` from the HTTP adaptation in `Http.Connections.Client`; the higher-level `Client` project composes them. The TypeScript and Java trees have their own transport and protocol implementations rather than wrapping those .NET services. + +Platform capability remains below the shared hub contract. Browser WebSockets and Server-Sent Events cannot set arbitrary request headers, so authentication may use transport-specific access-token alternatives. That restriction does not apply in the same way to Long Polling or non-browser clients. Cookie, proxy, certificate, credential, and WebSocket configuration also depend on the actual runtime and transport, including browser-hosted .NET. + +Handler registration and stream subscriptions are distinct lifetimes. In Java, `Subscription.unsubscribe()` removes a hub-method handler; RxJava `Disposable.dispose()` cancels a stream subscription. Each client owns completing or failing pending invocations, propagating stream cancellation, and handling registered callbacks when it stops or reconnects. The shared wire protocol does not supply a universal subscription or disposal abstraction. + +## Design Principles and Invariants + +- **Keep transport, encoding, dispatch, and routing independent.** Transport code carries data, protocol code interprets message shape, the dispatcher executes application operations, and the lifetime manager addresses clients. Cross-layer features use explicit connection features and contracts rather than moving one layer's policy into another. +- **Treat wire semantics as the cross-implementation boundary.** .NET, TypeScript, and Java communicate through versioned protocols. Defaults, framing, tolerated extensions, completion semantics, and error disclosure can affect interoperability even when public method signatures are unchanged. +- **Make ownership follow lifetime.** Connection state belongs to the connection, invocation state to the invocation, and provider state to its lifetime manager. Activated hubs and filters do not acquire connection lifetime merely because they can access a caller context. +- **Separate operation failure from connection failure.** Invocation completion, stream cancellation, graceful close, transport loss, and failed startup carry different meanings. Their error and cleanup paths preserve those distinctions. +- **Keep concurrent work accountable.** Returning to the message loop does not end an active stream or release its scope. Cancellation, completion, buffering, and shutdown need explicit owners even when work continues asynchronously. +- **Do not infer reliability from topology.** Scaleout expands addressing; ordinary reconnect starts again; stateful reconnect resumes retained state. None of these mechanisms alone provides durable delivery, application transaction acknowledgement, or unrestricted replay. +- **Model capabilities where they are provided.** Transfer formats, reconnect support, browser restrictions, and dynamic-code requirements are properties of particular layers and implementations, not consequences of speaking the Hub Protocol. + +## Architecture Documentation Map + +This document owns the area-wide runtime composition and its boundaries. The existing focused documents below retain their separate purposes; they are not duplicate architecture entry points. + +| Document | Boundary or purpose | +| --- | --- | +| [SignalR README](README.md) | Area introduction, product documentation, and development entry points | +| [Hub Protocol](docs/specs/HubProtocol.md) | Hub handshake, message semantics, JSON and MessagePack wire representations | +| [Transport Protocols](docs/specs/TransportProtocols.md) | HTTP negotiation and transport wire behavior | +| [JavaScript unit tests](docs/JSUnitTests.md) | Existing workflow documentation for isolated TypeScript client tests | +| [JavaScript functional tests](docs/JSFunctionalTests.md) | Existing workflow documentation for hosted JavaScript client/server tests | + +The source links in [Architectural Layers and Ownership](#architectural-layers-and-ownership) identify the implementations behind these boundaries. Samples, benchmarks, and test infrastructure support understanding and validation but are not additional runtime layers. + +## Verification Boundaries + +Different test boundaries establish different architectural claims. An isolated protocol test can establish parsing and framing without establishing that an HTTP transport produces the disputed input. A client unit test can establish its response to a disconnect without establishing browser behavior. Hosted tests join those owners when the observable behavior crosses the boundary. + +| Architectural behavior | Existing verification boundary | +| --- | --- | +| Shared .NET framing, handshake, and hub-message encodings | [`common/SignalR.Common/test`](common/SignalR.Common/test) | +| Negotiation, transport selection, HTTP connection lifetime, and individual server transports | [`common/Http.Connections/test`](common/Http.Connections/test) | +| Hub dispatch, options, filters, connection lifetime, and server reconnect integration | [`server/SignalR/test`](server/SignalR/test), including `HubConnectionHandlerTests` and `HubFilterTests` | +| Single-server and provider-independent cross-server lifetime-manager contracts | [`server/Specification.Tests`](server/Specification.Tests) | +| Redis-specific routing and backplane integration | [`server/StackExchangeRedis/test`](server/StackExchangeRedis/test) | +| .NET client state, HTTP adaptation, and hosted client/server behavior | [`clients/csharp/Client/test`](clients/csharp/Client/test), with separate unit and functional boundaries | +| TypeScript client and MessagePack implementation behavior | [`clients/ts/signalr/tests`](clients/ts/signalr/tests) and [`clients/ts/signalr-protocol-msgpack/tests`](clients/ts/signalr-protocol-msgpack/tests) | +| Hosted browser and JavaScript client/server behavior | [`clients/ts/FunctionalTests`](clients/ts/FunctionalTests) | +| Java client behavior | [`clients/java/signalr/test`](clients/java/signalr/test) | +| Protocol and dispatch costs or behavior under sustained load | [`perf/Microbenchmarks`](perf/Microbenchmarks) and [`perf/benchmarkapps/Crankier`](perf/benchmarkapps/Crankier) | + +The relevant observations are boundary-specific: parsed messages, protocol completions, transport termination, callback ordering, group delivery, pending invocation outcomes, cancellation, cleanup, and resource use. Shared conformance tests do not establish every provider or client implementation's behavior, and unit tests that run with reflection available do not establish trimming or Native AOT compatibility. These are limits of the evidence, not additional guarantees inferred from passing tests. + +## Terminology + +- **Hub connection** - The logical client/server relationship above a transport, with a selected hub protocol, caller identity, active operations, and connection-owned state. +- **Transport** - The mechanism carrying data. It may be a full-duplex WebSocket or a receive-side HTTP mechanism combined with separate sends. +- **Negotiation** - The HTTP-level agreement supplying transport choices, connection correlation information, and negotiated capabilities. +- **Hub handshake** - The initial JSON-framed exchange selecting the hub protocol and version on an established transport connection. +- **Transfer format** - The text or binary capability required to carry a protocol's encoded messages. +- **Hub protocol** - The message semantics and encoding for invocations, streams, completion, cancellation, and connection control. +- **Lifetime manager** - The server routing abstraction that tracks connections and implements client, group, and user addressing. +- **Backplane** - The server-to-server messaging mechanism used by a scaleout lifetime manager. +- **Stateful reconnect** - Resumption of a retained logical connection with acknowledgement, sequence tracking, and buffered resend; distinct from starting a new connection. From f11415696dac2016b934afc5bb22872059b709d6 Mon Sep 17 00:00:00 2001 From: PureWeen <223556219+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:12:26 -0500 Subject: [PATCH 3/6] Clarify SignalR parser compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/SignalR/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SignalR/ARCHITECTURE.md b/src/SignalR/ARCHITECTURE.md index f48a9a527ea9..f5b474ba6798 100644 --- a/src/SignalR/ARCHITECTURE.md +++ b/src/SignalR/ARCHITECTURE.md @@ -134,7 +134,7 @@ JSON and MessagePack express the same logical message families: invocation, stre Text messages end with the record separator. Binary messages have a length prefix. Transport reads and hub-message boundaries need not align: a read may contain part of one message or several complete messages. Framing owns identifying complete messages from segmented input and rejecting invalid lengths; protocol parsing owns interpreting the framed payload. -Forward-compatible parsing is part of the encoding contract. The .NET protocol parsers tolerate unknown JSON properties and unknown hub-message types, and their MessagePack parser permits additional trailing array elements. These tolerances must not be assumed across all client implementations. This extensibility is different from accepting malformed required fields or invalid framing. Tightening parsing can break an older implementation's ability to communicate with a newer peer even when the messages it understands have not changed. +The normative Hub Protocol treats unrecognized fields as protocol errors. Some .NET parser implementations are deliberately more permissive: they ignore unknown JSON properties and unknown hub-message types, and the MessagePack parser permits additional trailing array elements. These are implementation behaviors, not wire-contract guarantees, and they must not be assumed across client implementations. Tightening them can still break scenarios that rely on the current .NET behavior even when the messages they understand have not changed. ## Server Dispatch and Lifetime From c869c507405bdd2deadb6f70ec47bd5b2c985c24 Mon Sep 17 00:00:00 2001 From: PureWeen <223556219+Copilot@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:51:54 -0500 Subject: [PATCH 4/6] Link SignalR architecture from cross-cutting reviewer guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/CrossCuttingGuidance.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/CrossCuttingGuidance.md b/docs/CrossCuttingGuidance.md index c3d97b30c04d..2ddeea7f66f0 100644 --- a/docs/CrossCuttingGuidance.md +++ b/docs/CrossCuttingGuidance.md @@ -3,6 +3,10 @@ This guidance covers ASP.NET Core source work. Consult the relevant sections alongside the requested task and applicable repository and area instructions. +Area architecture overviews: + +- [SignalR](../src/SignalR/ARCHITECTURE.md) + ## Overarching principles - Preserve compatibility and public API discipline over local convenience. New APIs, constructors, options, packages, templates, analyzer IDs, and shared-framework metadata become long-lived contracts. From 4d523a37397c9156e1338f979f6f459c9e763995 Mon Sep 17 00:00:00 2001 From: PureWeen <223556219+Copilot@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:44:42 -0500 Subject: [PATCH 5/6] Remove SignalR link from cross-cutting guidance Keep this PR scoped to the SignalR architecture reference doc. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/CrossCuttingGuidance.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/CrossCuttingGuidance.md b/docs/CrossCuttingGuidance.md index 2ddeea7f66f0..c3d97b30c04d 100644 --- a/docs/CrossCuttingGuidance.md +++ b/docs/CrossCuttingGuidance.md @@ -3,10 +3,6 @@ This guidance covers ASP.NET Core source work. Consult the relevant sections alongside the requested task and applicable repository and area instructions. -Area architecture overviews: - -- [SignalR](../src/SignalR/ARCHITECTURE.md) - ## Overarching principles - Preserve compatibility and public API discipline over local convenience. New APIs, constructors, options, packages, templates, analyzer IDs, and shared-framework metadata become long-lived contracts. From 0109fb7702842fe092686998da79b41857a5f8cf Mon Sep 17 00:00:00 2001 From: PureWeen <223556219+Copilot@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:36:37 -0500 Subject: [PATCH 6/6] Address SignalR architecture review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/SignalR/ARCHITECTURE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SignalR/ARCHITECTURE.md b/src/SignalR/ARCHITECTURE.md index f5b474ba6798..1c412e40346d 100644 --- a/src/SignalR/ARCHITECTURE.md +++ b/src/SignalR/ARCHITECTURE.md @@ -110,6 +110,8 @@ Once the transport connection is open, the client's first hub-level message is a The selected hub protocol belongs to that connection for its lifetime. Negotiation protocol versions and hub protocol versions govern separate contracts. A handshake rejection or timeout ends startup before the connection enters normal hub lifetime processing; opening an HTTP transport alone does not create a successfully connected hub client. +Changes to these wire contracts should preserve interoperability between older clients and newer servers, and between newer clients and older servers. Respect HTTP `negotiateVersion` and hub protocol version checks when adding capabilities; for example, the [.NET client](clients/csharp/Client.Core/src/HubConnection.cs) sends hub protocol version 1 when stateful reconnect is not negotiated and the protocol supports it. + ## Transport and Protocol Separation ### Transport Composition and Transfer Formats @@ -150,7 +152,7 @@ Each hub invocation has its own activation and service scope. A streaming invoca Ordinary non-streaming hub invocations are serialized per connection by default. `HubOptions.MaximumParallelInvocationsPerClient` changes that invocation limit; it does not impose a single global lock on the server or make hub instances connection-scoped. -Invocations with streaming parameters or streaming results do not consume this non-streaming invocation limit. The dispatcher can continue processing messages while a stream is active, which is necessary to receive upload items, cancellation, and other connection traffic. Stream ownership and completion therefore cannot be inferred from the completion of the dispatch call that started the stream. +Invocations with streaming parameters or streaming results do not consume this non-streaming invocation limit. The dispatcher can continue processing messages while a stream is active, which is necessary to receive upload items, cancellation, and other connection traffic. Stream ownership and completion therefore cannot be inferred from the completion of the dispatch call that started the stream. See [Limit per-connection streaming invocations](https://learn.microsoft.com/aspnet/core/signalr/hubs#limit-per-connection-streaming-invocations) for application-enforced streaming limits. Cancellation crosses both invocation and connection boundaries. Invocation cancellation targets the corresponding active operation; connection abortion signals that the connection can no longer support its work. Stream completion, linked cancellation, hub release, and scope disposal remain owned by the invocation and connection machinery, including when application code observes cancellation asynchronously. @@ -191,6 +193,8 @@ Ordinary automatic reconnect is a different client policy: it attempts to establ ## Scaleout, Groups, and Users +[Azure SignalR Service (ASRS)](https://learn.microsoft.com/azure/azure-signalr/signalr-overview) is a managed scale-out option. New SignalR features should be designed to work with it. + `DefaultHubLifetimeManager` tracks and addresses connections in the current process. Broadcasts, group sends, user sends, and connection-targeted sends resolve to local connection contexts. Group membership is associated with connections, while user addressing uses the user identifier supplied for those connections; a user may have multiple connections. `RedisHubLifetimeManager` keeps local connection ownership but extends routing using Redis channels for hub-wide, group, user, and connection messages. Messages received from Redis are delivered through the local connection contexts and their selected hub protocols. Backplane serialization is an internal server-to-server contract, distinct from the hub encoding selected by each client.