Repository navigation
Releases: cirreum/Cirreum.RemoteConnections.SignalR
Release list
Release v2.0.2
Full Changelog: v2.0.1...v2.0.2
Release v2.0.1
Full Changelog: v2.0.0...v2.0.1
Release v2.0.0
Cirreum.RemoteConnections.SignalR 2.0.0 — what the first consumer found
Why this release exists
The first application built on 1.0 — a Blazor WASM portal against its own SignalR hub — reported
four things. Three are answered here; the fourth needs its own design.
The framework's default credential could not authenticate a first-party API. The ambient source
took no parameters, so it could not be told which connection it was minting for, and where the host
could not infer an audience it answered with its own defaults. On WebAssembly those are Microsoft
Graph scopes, so the credential a connection got out of the box was a Graph token — rejected by the
application's own API, and rejected at the server, so it read as an application authentication bug.
Two-argument callbacks were unreachable. A hub declaring ReceiveToolComplete(string, bool)
invokes it with two arguments; On<T> binds one. The workaround was to drop to the protected
HubConnection — which the base deliberately exposes, but it means the abstraction covered the easy
case and handed back the common one.
There was no way to await a hub method that returns nothing. SendAsync completes when the
message is sent; InvokeAsync<TResult> requires a result type.
What's new
The credential source is told what it is supplying for.
IRemoteConnectionCredentialSource.GetCredentialAsync receives a RemoteConnectionTokenRequest —
the endpoint, the Scopes the connection's options declare, and the connection type — and returns
AuthorizationHeaderSettings? rather than a bare token string.
An application names the audience on the options and writes no source at all:
options.Scopes = ["api://contoso/access_as_user"];A source may be registered keyed to a connection type, and is preferred over the unkeyed
registration for that connection, so one connection can use a different mechanism or identity
provider than another.
On<T1,T2> through On<T1..T8>. SignalR's protocol carries an argument array; these bind it.
A non-generic InvokeAsync, completing when the hub method does.
The behavioural change to read before deploying
A resolved credential now has three answers: a value to present, AuthorizationHeaderSettings.None
to connect deliberately without one, and null meaning none is available — which fails the
connect, naming the endpoint.
In 1.0 the last two were the same answer. A source returning nothing produced a connection that
opened, sent unauthenticated requests, and failed at the server. Separating them is what turns a
misconfigured credential from a puzzle into a message.
If a connection is meant to be anonymous, say so with None.
Not in this release
Binding a shared client interface — declaring IChatClientEvents once and having both ends use
it, rather than restating every method name as a string on the client. It is the most valuable of
the four reports and the largest: SignalR's .NET client has no strongly-typed client, and
reflection-based binding is the wrong answer under WebAssembly trimming, so it wants a source
generator and its own decision record.
Compatibility
Three mechanical changes — a namespace, a generic type argument on
SignalRRemoteConnectionContext.Create, and the credential seam — plus the behavioural change
above. See MIGRATION-v2.md.
Applications registering through Cirreum.Runtime.RemoteConnections.SignalR do not touch Create
directly, and feel this as the namespace change and the credential seam only.
See also
Cirreum.Contracts5.0.0 — the contracts, and the reasoning behind the credential shape.Cirreum.Runtime.Wasm— the scope-aware WebAssembly credential source that made the default work.
Release v1.0.1
Release v1.0.0
Cirreum.RemoteConnections.SignalR 1.0.0
First release of the SignalR transport for Cirreum's caller-side connection abstraction.
What this is for
IRemoteConnection describes a long-lived bidirectional connection: connect, subscribe to
inbound messages, send outbound ones, observe state. Until now nothing implemented it, so an
application that wanted a hub client wrote the plumbing itself — reconnection policy,
connection-state tracking, access-token wiring, disposal.
That plumbing is where behaviour drifts between applications, and reconnection and token
refresh are the parts that drift worst, because their failure modes appear only under real
network conditions.
What it provides
Derive from SignalRRemoteConnection and expose the endpoint's methods as typed members:
public sealed class ChatConnection(SignalRRemoteConnectionContext context)
: SignalRRemoteConnection(context) {
public IDisposable OnMessage(Func<ChatMessage, Task> handler) =>
this.On("ReceiveMessage", handler);
public Task SendMessageAsync(ChatMessage message, CancellationToken ct = default) =>
this.SendAsync("SendMessage", message, ct);
public Task<string> StartConversationAsync(string context, CancellationToken ct = default) =>
this.InvokeAsync<string>("StartConversation", [context], ct);
}The base owns lifetime, the state machine, reconnection, credential refresh and disposal.
Beyond the neutral contract
IRemoteConnection.SendAsync<T> carries a single payload as a hub method's single argument.
A real hub surface needs more than that, so two protected members complete it: a multi-argument
SendAsync for methods taking several parameters, and InvokeAsync<TResult> for methods that
return a value.
Neither is lifted onto IRemoteConnection. Request/response is a capability SignalR provides
and raw WebSockets do not, so it belongs to the transport type rather than to a contract every
transport must honour — and rather than being absent, which would leave every consumer
rebuilding correlation, timeouts and cancellation that the transport already has.
Connection identity
ConnectionId is assigned by the adapter and stable for the connection's life, because the
contract promises that and HubConnection's own identifier changes on every reconnect. The
transport's value is exposed separately as ServerConnectionId, for correlating with server
logs.
Credentials
Resolved on every connect and reconnect attempt through SignalR's own
HttpConnectionOptions.AccessTokenProvider, which it invokes on each negotiate. A token
therefore refreshes across reconnects with no framework code — the stale-token-after-reconnect
failure is dissolved rather than handled.
Postures resolve in a fixed order: an explicit callback on the options, an explicit
authorization header, an explicit choice to connect without credentials, then an ambient
IRemoteConnectionTokenSource registered by the host. With none of those available the
connection fails at ConnectAsync rather than connecting anonymously.
A credential is presented verbatim, so a scheme prefix carried inside it — part of the opaque
secret its issuer minted and stored — continues to route dispatch.
Reconnection
CappedJitterRetryPolicy retries indefinitely, backing off through a fixed schedule to a
configurable ceiling with jitter. SignalR's default stops after four attempts, which strands a
connection a user expects to stay open at Disconnected with no further attempt.
OnReconnectedAsync is where server-side session state that does not survive a transport
reconnect gets restored — group membership, presence announcements. Without a defined place
for that work the failure is quiet: the reconnect succeeds, the connection reports Connected,
and messages the caller expects never arrive.
Escape hatch
Nothing the transport can do is hidden. The HubConnection is available to derived types, and
the native IHubConnectionBuilder is exposed through a configure delegate that runs after the
framework has configured it, so any setting may be overridden.
Registration
Construct a context and register the connection type:
services.AddSingleton(sp => new ChatConnection(
SignalRRemoteConnectionContext.Create(sp, new RemoteConnectionOptions("MyApp") {
EndpointUri = new Uri("https://api.example.com/hubs/chat")
})));Options are validated at registration — a missing or relative endpoint, or a non-positive
reconnect ceiling, fails there rather than at first connect.
Requirements
Cirreum.Domain4.3.1 or later, which carriesRemoteConnectionBaseand the connection
lifecycle hooks.Cirreum.Contracts4.6.0 or later, which carriesIRemoteConnection,
RemoteConnectionOptionsandIRemoteConnectionTokenSource. It flows in transitively.