Cirreum.Runtime.Invocation.SignalR 1.1.1

dotnet add package Cirreum.Runtime.Invocation.SignalR --version 1.1.1
                    
NuGet\Install-Package Cirreum.Runtime.Invocation.SignalR -Version 1.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Cirreum.Runtime.Invocation.SignalR" Version="1.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Cirreum.Runtime.Invocation.SignalR" Version="1.1.1" />
                    
Directory.Packages.props
<PackageReference Include="Cirreum.Runtime.Invocation.SignalR" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Cirreum.Runtime.Invocation.SignalR --version 1.1.1
                    
#r "nuget: Cirreum.Runtime.Invocation.SignalR, 1.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Cirreum.Runtime.Invocation.SignalR@1.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Cirreum.Runtime.Invocation.SignalR&version=1.1.1
                    
Install as a Cake Addin
#tool nuget:?package=Cirreum.Runtime.Invocation.SignalR&version=1.1.1
                    
Install as a Cake Tool

Cirreum Runtime Invocation SignalR

NuGet Version NuGet Downloads GitHub Release License .NET

Runtime Extensions package for the Cirreum SignalR invocation source.

Overview

Cirreum.Runtime.Invocation.SignalR is the L5 Runtime Extensions package that surfaces app-facing extension methods for wiring SignalR Hubs into Cirreum's unified IInvocationContext seam. It supplies three extension methods, each on the framework type that makes them most discoverable:

  • AddSignalRInvocation() on IHostApplicationBuilder — registers the SignalR invocation source (marker-dedup'd) and opens the IInvocationBuilder scope for per-instance Hub bindings.
  • AddSignalR<THub>(instanceKey) on IInvocationBuilder — captures THub at the call site, stashes the (instanceKey, typeof(THub)) pair as a SignalRHubMapping in DI for the L3 registrar to resolve.
  • MapSignalRInvocation() on IEndpointRouteBuilder — invokes every InvocationProviderMapping whose ProviderName matches SignalRInvocationRegistrar.ProviderKey, walking enabled instances and mapping each Hub at its configured path.

Apps install this package directly. It transitively pulls the L3 Cirreum.Invocation.SignalR (registrar, settings, HubFilter, connection adapter that implements IInvocationConnection.SendAsync<T>) and the L4 Cirreum.Runtime.InvocationProvider (helper, scope object).

Architectural position

L2 Core
  Cirreum.InvocationProvider               ← abstractions: IInvocationContext, registrar base, ...

L3 Infrastructure
  Cirreum.Invocation.SignalR               ← registrar, settings, HubFilter, connection adapter
  Cirreum.Invocation.WebSockets            ← peer for raw WebSockets

L4 Runtime
  Cirreum.Runtime.InvocationProvider       ← IInvocationBuilder scope object, RegisterInvocationProvider helper

L5 Runtime Extensions
  Cirreum.Runtime.Invocation.SignalR       ← THIS PACKAGE — AddSignalRInvocation, AddSignalR<THub>, MapSignalRInvocation
  Cirreum.Runtime.Invocation               ← umbrella (AddInvocation, MapInvocation across all sources)

Mirrors the Identity track's Cirreum.Runtime.Identity.Oidc shape — same SRP-split, same per-protocol/umbrella relationship, same generic-method-with-key per-instance binding pattern.

What's in the box

Extension Lives on Role
AddSignalRInvocation(this IHostApplicationBuilder, Action<IInvocationBuilder>?) (Microsoft.Extensions.Hosting) IHostApplicationBuilder Top-level entry point. Marker-dedup'd registration of the SignalR invocation source; opens the IInvocationBuilder scope for per-instance Hub bindings.
AddSignalR<THub>(this IInvocationBuilder, string instanceKey) (Cirreum.Invocation) IInvocationBuilder Per-instance Hub binding. Captures THub at the call site; stashes a SignalRHubMapping singleton in DI for the L3 registrar to resolve at endpoints-phase time.
MapSignalRInvocation(this IEndpointRouteBuilder) (Microsoft.AspNetCore.Builder) IEndpointRouteBuilder Endpoints-phase entry point. Resolves SignalR-tagged InvocationProviderMapping records and invokes their deferred Map closures. Pairs naturally with ASP.NET's built-in MapHub<THub>().

How registration works

The AddSignalRInvocation() extension does two things:

  1. Marker-dedup'd: registers the SignalR invocation source by calling builder.RegisterInvocationProvider<SignalRInvocationRegistrar, SignalRInvocationSettings, SignalRInvocationInstanceSettings>() from the L4 helper. The L4 helper:
    • Binds Cirreum:Invocation:Providers:SignalR from IConfiguration to SignalRInvocationSettings.
    • Calls registrar.Register(...) — services phase — which calls services.AddSignalR() and registers the InvocationContextHubFilter against the global HubOptions.
    • Stashes an InvocationProviderMapping in DI capturing the deferred registrar.Map(...) closure.
  2. Opens the IInvocationBuilder scope for the configure callback so apps can chain AddSignalR<THub>(instanceKey) calls per Hub-type.

Inside the configure callback, each AddSignalR<THub>(instanceKey) call:

  1. Captures THub as a generic argument at the call site (this is the whole reason the per-instance call needs to be a generic method — THub can't come from JSON).
  2. Stashes a SignalRHubMapping(instanceKey, typeof(THub)) singleton in DI.

MapSignalRInvocation() resolves all InvocationProviderMapping records with ProviderName == SignalRInvocationRegistrar.ProviderKey and invokes their Map closures. The L3 registrar's MapSource:

  1. Resolves the SignalRHubMapping for each enabled instance from configuration.
  2. Dispatches through reflection to endpoints.MapHub<THub>(settings.Path) (ASP.NET ships only a generic-only overload).
  3. Wires RequireAuthorization with AuthenticationSchemes = settings.Scheme if a Scheme is set.

Configuration

SignalR exposes three configuration surfaces — Cirreum mirrors each as an explicit, named sub-section. Cirreum framework fields (Enabled, Path, Scheme) live at the instance-section root; SignalR-native options are namespaced under HubOptions and HttpOptions sub-sections so the three roles never collide.

{
  "Cirreum": {
    "Invocation": {
      "Providers": {
        "SignalR": {

          "HubOptions": {
            "KeepAliveInterval": "00:01:00",
            "ClientTimeoutInterval": "00:02:00",
            "EnableDetailedErrors": false
          },

          "Instances": {
            "chat": {
              "Enabled": true,
              "Path": "/chat",
              "Scheme": "oidc_primary",

              "HubOptions": {
                "MaximumReceiveMessageSize": 65536,
                "MaximumParallelInvocationsPerClient": 4
              },

              "HttpOptions": {
                "Transports": "WebSockets, LongPolling",
                "ApplicationMaxBufferSize": 131072,
                "TransportMaxBufferSize": 131072,
                "LongPolling": {
                  "PollTimeout": "00:01:30"
                },
                "WebSockets": {
                  "CloseTimeout": "00:00:10"
                }
              }
            },
            "notifications": {
              "Enabled": true,
              "Path": "/notifications",
              "Scheme": "oidc_primary"
            }
          }

        }
      }
    }
  }
}
Sub-section Binds to Scope
Providers:SignalR:HubOptions HubOptions (global) Defaults applied to every Hub in the host
Providers:SignalR:Instances:{key}:HubOptions HubOptions<THub> (per-Hub) Per-Hub overrides for this instance's THub
Providers:SignalR:Instances:{key}:HttpOptions HttpConnectionDispatcherOptions (per-mapping) Transport-level config for this instance's MapHub endpoint

Precedence for Hub-level options is defaults → global HubOptions → per-Hub HubOptions<THub> — handled by ASP.NET's HubOptionsSetup<THub> automatically. HttpConnectionDispatcherOptions is per-mapping only — it has no global form in SignalR's design.

The instance key ("chat", "notifications") is what AddSignalR<THub>(instanceKey) binds against — match the call-site key to the configured instance name.

Scheme references a configured Authorization instance under Cirreum:Authorization:Providers:*:Instances:{Scheme}. Optional — leave unset for unauthenticated hubs (rare).

Why explicit sub-sections

HubOptions and HttpConnectionDispatcherOptions are different SignalR types configuring different layers (Hub method invocation vs. HTTP connection dispatch). They share zero property names today, but a flat-binding pattern would still leave intent ambiguous to readers and risk silent collisions if Microsoft ever ships overlapping properties. The explicit sub-sections make the layer split visible in the JSON itself and let JSON schemas validate each sub-section strictly.

One Hub type per instance

AddSignalR<THub>(instanceKey) allows each THub to be mapped to exactly one instance key per host. ASP.NET's HubOptions<THub> is per-Hub-type — there is no per-instance bucket — so mapping the same Hub class to multiple instance keys would silently accumulate HubOptions overrides across both sections (last write wins per property), which is confusing rather than meaningful.

To expose the same Hub at multiple paths with different settings, subclass it:

public sealed class PublicChatHub : ChatHub { }
public sealed class PrivateChatHub : ChatHub { }

builder.AddSignalRInvocation(b => b
    .AddSignalR<PublicChatHub>("public")
    .AddSignalR<PrivateChatHub>("private"));

Each subclass gets its own HubOptions<T> and its own DI registration; the inherited Hub method bodies are unchanged. Attempting to map the same THub twice throws InvalidOperationException with a message pointing to this workaround.

Server-initiated push

From a SignalR Hub method (or any code running inside the SignalR invocation pipeline — including Conductor command/query handlers triggered from a Hub method), push to the calling client through the ambient IInvocationContextAccessor.Current.Connection:

public sealed class ChatHub(IInvocationContextAccessor accessor) : Hub {
    public async Task Echo(string text) {
        var connection = accessor.Current?.Connection;
        if (connection is not null) {
            await connection.SendAsync("Echo", new { text, at = DateTime.UtcNow });
        }
    }
}

// AsyncLocal flows the invocation through Conductor — the same handler
// code can run from HTTP or SignalR; the connection is non-null only when
// there's a long-lived calling connection.
public sealed class GenerateReportHandler(
    IInvocationContextAccessor accessor) : ICommandHandler<GenerateReportCommand> {

    public async ValueTask<Result> Handle(GenerateReportCommand cmd, CancellationToken ct) {
        var connection = accessor.Current?.Connection;
        if (connection is not null) {
            await connection.SendAsync("Progress", new { Percent = 0,   Stage = "Loading" }, ct);
        }
        // ... work ...
        if (connection is not null) {
            await connection.SendAsync("Progress", new { Percent = 100, Stage = "Done" }, ct);
        }
        return Result.Success(/* ... */);
    }

}

The no-method SendAsync<T>(payload) overload uses the runtime type name as the SignalR method-routing convention (e.g. SendAsync(new ChatMessage(...)) dispatches to client connection.on("ChatMessage", ...)); the keyed SendAsync<T>(method, payload) overload accepts an explicit method name. Serialization flows through SignalR's configured IHubProtocol (JSON or MessagePack — set via AddSignalR().AddJsonProtocol(...) / .AddMessagePackProtocol()).

Hub method bodies that already use the SignalR-native Clients.Caller.SendAsync(...) API are equivalent — both paths target the same SignalR pipeline.

What Connection.SendAsync does and doesn't do

The connection's typed SendAsync<T> is bound to the active invocation — it pushes to the connection that delivered the currently-executing Hub method (or downstream Conductor handler invoked from one). It is not a general server-to-client push mechanism for arbitrary connections.

You want to Use
Push extra messages to the client that triggered this Hub method (progress, streaming partial results, multi-message responses) accessor.Current?.Connection?.SendAsync(...) (Cirreum-abstracted)
Push to a different connected client by ConnectionId IHubContext<THub>.Clients.Client(id).SendAsync(...) (SignalR-native)
Broadcast to all connected clients IHubContext<THub>.Clients.All.SendAsync(...) (SignalR-native)
Push to a SignalR group IHubContext<THub>.Clients.Group(name).SendAsync(...) (SignalR-native)
Push from a background service, timer, or inbound webhook (no active invocation) IHubContext<THub>.Clients.X.SendAsync(...) (SignalR-native) — the ambient connection is null when no active invocation exists
Push from a handler invoked via HTTP n/a — HTTP is request/response. IInvocationContext.Connection is null for HTTP. Use the handler's return value instead.

IHubContext<THub> is registered as a singleton by services.AddSignalR() — inject it anywhere, including code with no active invocation context. Cirreum doesn't abstract this surface because "push to arbitrary connection by id / group / all" varies wildly across long-lived transports (SignalR's rich Clients API, raw WebSocket's hand-rolled registries, gRPC streaming's one-stream-at-a-time model). The seam unifies identity, pipeline, and the calling-client reply path; transport-specific superpowers stay accessible through their native APIs.

Handlers that may run from both HTTP and SignalR

The null-check on accessor.Current?.Connection is the natural feature gate: when invoked from HTTP it's null and the push is skipped; when invoked from SignalR it's non-null and the push happens. Same handler, both transports, no special branching beyond the null check shown in the example above.

The HTTP caller gets the return value; the SignalR caller gets the progress stream and the return value.

Connection lifecycle

Implement IConnectionLifecycle (from Cirreum.Invocation.Connections) and register it in DI to receive OnConnectedAsync / OnDisconnectedAsync callbacks. The HubFilter dispatches both under a synthetic invocation scope so consumers like IUserStateAccessor work normally inside the callbacks. The DisconnectInfo parameter on OnDisconnectedAsync carries the disconnect circumstances — graceful close vs. abort, the underlying exception (if any), and a human-readable reason — populated by the L3 adapter from SignalR's optional Exception? parameter:

internal sealed class AuditConnectionLifecycle(ILogger<AuditConnectionLifecycle> logger)
    : IConnectionLifecycle {

    public ValueTask<bool> OnConnectedAsync(IInvocationConnection connection, CancellationToken ct) {
        // Inspect connection.User, connection.ConnectionId, connection.Items, etc.
        // Return false to reject the connection (the upgrade aborts; client sees normal rejection).
        return ValueTask.FromResult(true);
    }

    public ValueTask OnDisconnectedAsync(
        IInvocationConnection connection,
        DisconnectInfo info,
        CancellationToken ct) {

        if (info.WasGraceful) {
            logger.LogInformation("Connection {Id} closed cleanly", connection.ConnectionId);
        } else if (info.Exception is not null) {
            logger.LogWarning(info.Exception,
                "Connection {Id} aborted: {Reason}", connection.ConnectionId, info.Reason);
        }

        return ValueTask.CompletedTask;
    }

}

Per-transport mapping for DisconnectInfo: SignalR's Exception? parameter from OnDisconnectedAsync(HubLifetimeContext, Exception?) populates as WasGraceful = exception is null, Exception = exception, Reason = exception?.Message.

Deployment — works with Azure SignalR Service

Yes, this package works transparently with Azure SignalR Service. Azure SignalR Service is a transport-routing layer underneath SignalR — clients connect to Azure's edge, which forwards messages to your app server. Your Hub classes, IHubFilter, IHubContext<THub>, and Cirreum's invocation seam all use the same public SignalR contracts in either deployment model, so the same Cirreum-wired Hubs run unchanged on either.

The only difference is one extra service registration — the standard Microsoft AddAzureSignalR() extension from the Microsoft.Azure.SignalR package:

var builder = DomainApplication.CreateBuilder(args);

builder.AddSignalRInvocation(b => b
    .AddSignalR<ChatHub>("chat"));

// Wire Azure SignalR Service. Connection string flows through the standard
// IConfiguration chain — Cirreum.Secrets.Azure transparently resolves from
// KeyVault / env vars / user secrets / appsettings.
builder.Services.AddSignalR()
    .AddAzureSignalR(builder.Configuration.GetConnectionString("Azure:SignalR"));

using var app = builder.Build<MyDomainMarker>();
app.UseDefaultMiddleware();
app.MapSignalRInvocation();
await app.RunAsync();

Everything else — InvocationContextHubFilter publishing IInvocationContext per Hub method invocation, SignalRConnection materialization, IInvocationConnection.SendAsync for in-invocation push, IConnectionLifecycle for connect/disconnect callbacks, IHubContext<THub> for out-of-band push — keeps working identically. The Cirreum framework code doesn't know or care whether SignalR is self-hosted or routed through Azure SignalR Service.

There is intentionally no separate Cirreum.Invocation.SignalR.Azure package. Azure SignalR Service is a deployment topology, not a different SignalR implementation — the same Cirreum SignalR L3+L5 packages serve both. We add per-implementation Cirreum packages only when there's framework-specific integration code to write (the Authorization track has separate Oidc / Entra / External packages because OIDC and Entra have genuinely different claim shapes, trust roots, and validation paths to abstract). For Azure SignalR Service vs. self-hosted SignalR, Microsoft's own API already abstracts the difference.

Dependencies

  • Cirreum.Runtime.InvocationProvider 1.1.0+ — L4 helper (IInvocationBuilder scope object, RegisterInvocationProvider<> helper, InvocationProviderMapping record)
  • Cirreum.Invocation.SignalR 1.2.0+ — L3 registrar, settings, HubFilter, connection adapter (with typed SendAsync<T> overloads), SignalRInvocationRegistrar.ProviderKey const
  • Microsoft.AspNetCore.App (framework reference) — SignalR (Microsoft.AspNetCore.SignalR), endpoint routing

Versioning

Follows Semantic Versioning. Major bumps are coordinated with the L3 Cirreum.Invocation.SignalR and the L2 Cirreum.InvocationProvider packages.

License

MIT — see LICENSE.


Cirreum Foundation Framework
Layered simplicity for modern .NET

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Cirreum.Runtime.Invocation.SignalR:

Package Downloads
Cirreum.Runtime.Invocation

Runtime Extensions umbrella package for the Cirreum Invocation source family. Composes Cirreum.Runtime.Invocation.SignalR and Cirreum.Runtime.Invocation.WebSockets, and exposes app-facing AddInvocation() / MapInvocation() extensions that register all sources and map all endpoints.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1 85 5/11/2026
1.1.0 94 5/10/2026
1.0.0 103 5/8/2026