Cirreum.Runtime.Invocation.WebSockets 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Cirreum.Runtime.Invocation.WebSockets --version 1.0.0
                    
NuGet\Install-Package Cirreum.Runtime.Invocation.WebSockets -Version 1.0.0
                    
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.WebSockets" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Cirreum.Runtime.Invocation.WebSockets" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Cirreum.Runtime.Invocation.WebSockets" />
                    
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.WebSockets --version 1.0.0
                    
#r "nuget: Cirreum.Runtime.Invocation.WebSockets, 1.0.0"
                    
#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.WebSockets@1.0.0
                    
#: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.WebSockets&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Cirreum.Runtime.Invocation.WebSockets&version=1.0.0
                    
Install as a Cake Tool

Cirreum Runtime Invocation WebSockets

NuGet Version NuGet Downloads GitHub Release License .NET

Runtime Extensions package for the Cirreum WebSocket invocation source.

Overview

Cirreum.Runtime.Invocation.WebSockets is the L5 Runtime Extensions package that surfaces app-facing extension methods for wiring raw WebSocket endpoints into Cirreum's unified IInvocationContext seam. It supplies three extensions, each on the framework type that makes them most discoverable, plus a slim builder for the optional companion request endpoint:

  • AddWebSocketInvocation() on IHostApplicationBuilder — registers the WebSocket invocation source (marker-dedup'd) and opens the IInvocationBuilder scope for per-instance handler bindings.
  • AddWebSocket<THandler>(instanceKey, request: ...) on IInvocationBuilder — captures THandler at the call site, registers it as scoped, and (optionally) configures the companion HTTP request endpoint via the request: builder.
  • MapWebSocketInvocation() on IEndpointRouteBuilder — invokes every InvocationProviderMapping whose ProviderName matches WebSocketInvocationRegistrar.ProviderKey, walking enabled instances and mapping each WebSocket endpoint (and its optional request endpoint) at the configured paths. Calls app.UseWebSockets() automatically.
  • IWebSocketRequestBuilder — slim, framework-specific minimal API surface for the request endpoint with Map(handler, configure?) (defaults to POST) and Map(httpMethod, handler, configure?) (explicit method) overloads.

Apps install this package directly. It transitively pulls the L3 Cirreum.Invocation.WebSockets (registrar, orchestrator, connection adapter, IConnectionSender impl, IWebSocketUrlBuilder) and the L4 Cirreum.Runtime.InvocationProvider (helper, scope object).

Architectural position

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

L3 Infrastructure
  Cirreum.Invocation.SignalR               ← peer for SignalR Hubs
  Cirreum.Invocation.WebSockets            ← registrar, orchestrator, connection adapter

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

L5 Runtime Extensions
  Cirreum.Runtime.Invocation.SignalR       ← peer for SignalR
  Cirreum.Runtime.Invocation.WebSockets    ← THIS PACKAGE
  Cirreum.Runtime.Invocation               ← umbrella (AddInvocation, MapInvocation across all sources)

Mirrors the SignalR L5 package's shape — same SRP-split, same per-source/umbrella relationship, same Add*Invocation + per-instance generic-method-with-key + Map*Invocation pattern.

What's in the box

Extension / Type Lives on Role
AddWebSocketInvocation(this IHostApplicationBuilder, Action<IInvocationBuilder>?) (Microsoft.Extensions.Hosting) IHostApplicationBuilder Top-level entry point. Marker-dedup'd registration of the WebSocket invocation source; opens the IInvocationBuilder scope for per-instance handler bindings.
AddWebSocket<THandler>(this IInvocationBuilder, string, Action<IWebSocketRequestBuilder>?) (Cirreum.Invocation) IInvocationBuilder Per-instance handler binding. Captures THandler at the call site, registers it as scoped, optionally configures the companion request endpoint via the request: builder.
IWebSocketRequestBuilder (Cirreum.Invocation) (passed as request: callback parameter) Slim minimal API surface for the request endpoint. Map(handler, configure?) defaults to POST; Map(httpMethod, handler, configure?) for explicit methods. The optional configure callback hooks into the real RouteHandlerBuilder for OpenAPI / naming / tags / additional metadata.
MapWebSocketInvocation(this IEndpointRouteBuilder) (Microsoft.AspNetCore.Builder) IEndpointRouteBuilder Endpoints-phase entry point. Calls app.UseWebSockets(), resolves WebSocket-tagged InvocationProviderMapping records, and invokes their deferred Map closures.

How registration works

The AddWebSocketInvocation() extension does two things:

  1. Marker-dedup'd: registers the WebSocket invocation source by calling builder.RegisterInvocationProvider<WebSocketInvocationRegistrar, WebSocketInvocationSettings, WebSocketInvocationInstanceSettings>() from the L4 helper. The L4 helper:
    • Binds Cirreum:Invocation:Providers:WebSocket from IConfiguration to WebSocketInvocationSettings.
    • Calls registrar.Register(...) — services phase — which binds IOptions<WebSocketInvocationSettings>, registers IWebSocketUrlBuilder, registers WebSocketOrchestrator, registers IConnectionSenderWebSocketConnectionSender, and validates per-instance settings (paths, hard caps).
    • Stashes an InvocationProviderMapping in DI capturing the deferred registrar.Map(...) closure.
  2. Opens the IInvocationBuilder scope for the configure callback so apps can chain AddWebSocket<THandler>(instanceKey, request: ...) calls per handler-type.

Inside the configure callback, each AddWebSocket<THandler>(instanceKey, request: ...) call:

  1. Validates THandler uniqueness — the same handler type cannot be mapped to two instance keys (throws on conflict).
  2. Resolves the request: builder if provided — captures the handler delegate, optional method override, and optional configure callback into a WebSocketRequestBuilder. Throws if the callback runs but never invokes Map(...).
  3. Stashes a WebSocketHandlerMapping(instanceKey, typeof(THandler), requestHandler, requestMethod, configureRequestRoute) singleton in DI.
  4. Registers THandler as a scoped service — one instance per connection.

MapWebSocketInvocation() calls app.UseWebSockets() then resolves all InvocationProviderMapping records with ProviderName == WebSocketInvocationRegistrar.ProviderKey and invokes their Map closures. The L3 registrar's MapSource:

  1. Validates RequestPathrequest: builder pairing — throws at startup with an actionable message if either is set without the other.
  2. Maps the WebSocket endpoint at Path using Map() so both GET (HTTP/1.1) and CONNECT (HTTP/2+) are accepted; excludes from OpenAPI/Swagger discovery.
  3. Maps the request endpoint at RequestPath (when configured) using MapMethods with the captured method (default POST), attaches WebSocketInstanceMetadata for IWebSocketUrlBuilder, and invokes the app's configure callback against the real RouteHandlerBuilder.
  4. Applies RequireAuthorization to both endpoints when Scheme is set.

Configuration

{
  "Cirreum": {
    "Invocation": {
      "Providers": {
        "WebSocket": {
          "Instances": {

            "media": {
              "Enabled": true,
              "Path": "/twilio/media-stream/{callSid}",
              "RequestPath": "/twilio/incoming-call",
              "Scheme": "twilio",
              "DisconnectTimeoutSeconds": 60,
              "MaxMessageSizeBytes": 65536,
              "KeepAliveInterval": "00:00:30",
              "KeepAliveTimeout": "00:00:10"
            },

            "telemetry": {
              "Enabled": true,
              "Path": "/ws/telemetry",
              "Scheme": "oidc_primary",
              "MaxMessageSizeBytes": 524288
            }

          }
        }
      }
    }
  }
}
Field Default Hard cap Purpose
Enabled false Per-instance gate
Path (required) WebSocket endpoint route template (supports {name} placeholders)
RequestPath null Optional companion HTTP endpoint that initiates the WebSocket flow
Scheme null References a configured Authorization instance; applies RequireAuthorization to both endpoints
DisconnectTimeoutSeconds 30 300 Cleanup budget for OnDisconnectedAsync hooks
MaxMessageSizeBytes 64 KB 8 MB Max bytes per complete message; oversize → MessageTooBig close
ReceiveBufferSizeBytes 4 KB 64 KB Initial pooled receive buffer per connection
KeepAliveInterval null Override WebSocketOptions.KeepAliveInterval (default 2 min)
KeepAliveTimeout null Override WebSocketOptions.KeepAliveTimeout (default 30 s)

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

Quick start — telemetry endpoint (single-phase)

var builder = WebApplication.CreateBuilder(args);

builder.AddWebSocketInvocation(b => b
    .AddWebSocket<TelemetryHandler>("telemetry"));

var app = builder.Build();
app.MapWebSocketInvocation();
app.Run();
public sealed class TelemetryHandler(IConnectionSender sender) : WebSocketHandler {

    public override async Task OnMessageAsync(
        IInvocationContext context,
        ReadOnlyMemory<byte> message,
        WebSocketMessageType messageType) {

        var batch = JsonSerializer.Deserialize<TelemetryBatch>(message.Span);
        await ProcessAsync(batch, context.Aborted);
        await sender.SendAsync("Ack", new { count = batch.Items.Count }, context.Aborted);
    }

}

Quick start — Twilio Media Streams (two-phase)

The IVA reference codebase pattern: Twilio webhooks the incoming-call URL, our server returns TwiML with a WebSocket URL, Twilio opens the WebSocket and streams audio frames.

Configuration — see the JSON above for the media instance.

Program.cs:

var builder = DomainApplication.CreateBuilder(args);

builder.AddWebSocketInvocation(b => b
    .AddWebSocket<TwilioMediaHandler>("media", request: r => r
        .Map(TwilioApi.HandleRequest, m => m
            .WithName("twilio-incoming-call")
            .WithTags("twilio")
            .Produces<string>(200, "text/xml"))));

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

Request delegate — full minimal API binding; IWebSocketUrlBuilder builds the absolute wss:// URL with {callSid} auto-extracted from form data:

public static class TwilioApi {

    public static async Task<IResult> HandleRequest(
        HttpContext context,
        IWebSocketUrlBuilder urls,
        ITwilioRequestValidator validator,
        DomainApiClient domainApi) {

        if (!validator.ValidateRequest(context)) return Results.Unauthorized();

        var callSid = context.Request.Form["CallSid"].ToString();
        var session = await domainApi.ClaimSessionAsync(callSid);
        if (!session.IsSuccess) return Results.Content(HangupTwiml(), "text/xml");

        // {callSid} auto-extracted from form data (case-insensitive name match)
        var streamUrl = urls.Build(context);

        return Results.Content($"""
            <?xml version="1.0" encoding="UTF-8"?>
            <Response>
                <Connect><Stream url="{streamUrl}" /></Connect>
            </Response>
            """, "text/xml");
    }

}

Handler:

// Note the symmetry: handler-bound `this.SendAsync(...)` for the inbound (Twilio) socket,
// direct `_aiSocket.SendAsync(...)` for the outbound (handler-owned) socket. No DI needed
// for the voice-frame hot path. IConnectionSender stays available for cross-cutting code
// (Conductor command handlers triggered from AI tool calls — see "Server-initiated push").
public sealed class TwilioMediaHandler(
    IAiClient ai,
    IDomainApi domain,
    ILogger<TwilioMediaHandler> logger) : WebSocketHandler {

    private ClientWebSocket? _aiSocket;
    private Task? _aiTask;
    private CallDisposition _disposition = CallDisposition.Resolved;

    public override async Task OnConnectedAsync(CancellationToken ct) {
        _aiSocket = await ai.ConnectAsync(ct);

        _aiTask = Task.Run(async () => {
            try {
                await foreach (var aiEvent in ReadAiEventsAsync(_aiSocket, ct)) {
                    if (TryExtractAudio(aiEvent, out var streamSid, out var audio)) {
                        var msg = TwilioMediaMessage.Create(streamSid, audio);
                        await SendAsync(msg, ct);   // ← handler-bound, works in this Task.Run
                    }
                }
            } finally {
                // AI side ended — terminate the inbound transport too
                Connection!.Abort();
            }
        }, ct);
    }

    public override async Task OnMessageAsync(
        IInvocationContext context,
        ReadOnlyMemory<byte> message,
        WebSocketMessageType messageType) {

        // Forward Twilio media frames to the AI socket. _aiSocket is handler-owned —
        // direct SendAsync; no framework involvement.
        await _aiSocket!.SendAsync(message, messageType, endOfMessage: true, context.Aborted);
    }

    public override async Task OnDisconnectedAsync(DisconnectInfo info, CancellationToken ct) {
        // ct is a bounded cleanup budget (default 30s, or host shutdown). On exhaustion, bail.
        if (info.Exception is not null) {
            _disposition = CallDisposition.Error;
            logger.LogError(info.Exception, "Call ended with error: {Reason}", info.Reason);
        }

        if (_aiSocket?.State == WebSocketState.Open) {
            try {
                await _aiSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Call ended", ct);
            } catch (OperationCanceledException) {
                logger.LogWarning("Cleanup budget exhausted closing AI socket");
                return;
            } catch (Exception ex) {
                logger.LogError(ex, "Error closing AI socket");
            }
        }

        if (_aiTask is not null) await _aiTask;

        await domain.CompleteCallAsync(info, _disposition, ct);
    }

}

The IWebSocketRequestBuilder — slim minimal API surface

public interface IWebSocketRequestBuilder {
    void Map(Delegate handler, Action<RouteHandlerBuilder>? configure = null);
    void Map(string httpMethod, Delegate handler, Action<RouteHandlerBuilder>? configure = null);
}
Overload Default Use
Map(Delegate, configure?) POST Webhook-style endpoints (Twilio, Stripe, GitHub all use POST).
Map(string method, Delegate, configure?) (explicit) GET-style negotiation, query-string token flows, or other rare cases.

The Delegate parameter accepts any minimal API delegate — inline lambdas, static method groups, instance method groups. Full minimal API parameter binding applies (DI services, HttpContext, route parameters, IFormFile, body binding, IResult return types, etc.):

// Inline lambda — DI services as parameters
.Map(async (HttpContext ctx, ISessionService sessions) => { ... })

// Static method group
.Map(TwilioApi.HandleRequest)

// Instance method group
.Map(_voiceApi.HandleRequest)

The configure callback hooks into the real RouteHandlerBuilder at endpoints-phase time. Inside it, every minimal API extension works: WithName, WithTags, Produces<T>, Accepts<T>, WithOpenApi, RequireAuthorization (additional schemes beyond the instance Scheme), DisableAntiforgery, WithMetadata, etc.

.Map(TwilioApi.HandleRequest, m => m
    .WithName("twilio-incoming-call")
    .WithTags("twilio")
    .Accepts<IFormCollection>("application/x-www-form-urlencoded")
    .Produces<string>(200, "text/xml")
    .WithOpenApi(op => {
        op.Description = "Twilio Programmable Voice incoming-call webhook.";
        return op;
    }))

IWebSocketUrlBuilder — building WebSocket URLs from path templates

For apps that need to embed the WebSocket URL in a request response (TwiML, JSON, etc.), inject IWebSocketUrlBuilder:

public interface IWebSocketUrlBuilder {
    string Build(HttpContext context, object? routeValues = null, object? queryValues = null);
    string Build(string instanceKey, HttpContext context, object? routeValues = null, object? queryValues = null);
}
Overload Instance source
Build(HttpContext, ...) Implicit — resolved from the active endpoint's WebSocketInstanceMetadata (only works inside a request endpoint).
Build(string instanceKey, HttpContext, ...) Explicit — for cross-instance scenarios or use outside a request endpoint.

Path-template values are resolved in priority order:

  1. Explicit routeValues parameter
  2. Request.RouteValues (URL route parameters)
  3. Request.Query (query string)
  4. Request.Form (form fields, only when content-type is form-encoded)

Names match case-insensitively — Twilio's CallSid form field auto-fills the {callSid} template placeholder. Scheme converts automatically: httpswss, httpws. Unresolved placeholders throw at build time rather than producing malformed URLs.

Server-initiated push

The framework offers two complementary push APIs. They route to the same client over the same wire format; the choice is about who's calling the push.

this.SendAsync(...) — handler-bound (handler-internal code)

Three protected overloads on WebSocketHandler:

protected ValueTask SendAsync<T>(T payload, CancellationToken ct = default);
protected ValueTask SendAsync<T>(string method, T payload, CancellationToken ct = default);
protected ValueTask SendAsync(
    ReadOnlyMemory<byte> payload,
    WebSocketMessageType type = WebSocketMessageType.Binary,
    CancellationToken ct = default);

Resolves the underlying WebSocket directly through this.Connection. No accessor lookup, no IInvocationContextAccessor dependency — works from any calling context inside the handler, including handler-managed Task.Run background loops, timers, and fire-and-forget continuations. The IVA-style voice example above uses this for the audio-frame hot path:

await SendAsync(twilioMediaMessage, ct);   // ← from inside Task.Run, just works

Use this for handler-internal push — by far the common case for WebSocket apps that own outbound state and forward it to the inbound caller (voice, telemetry, custom protocols).

IConnectionSender — ambient-invocation-aware (cross-cutting code)

Inject IConnectionSender from cross-cutting code that doesn't know its transport — Conductor command/query handlers, validators, transport-agnostic services. Resolves the active connection through IInvocationContextAccessor.Current, so the same handler code can push back over HTTP, SignalR, or WebSocket without knowing which it's running on:

public sealed class GenerateReportHandler(
    IInvocationContextAccessor accessor,
    IConnectionSender sender) : ICommandHandler<GenerateReportCommand> {

    public async ValueTask<Result> Handle(GenerateReportCommand cmd, CancellationToken ct) {
        var canPush = accessor.Current?.Connection is not null;

        if (canPush) await sender.SendAsync("Progress", new { Percent = 0, Stage = "Loading" }, ct);
        // ... work ...
        if (canPush) await sender.SendAsync("Progress", new { Percent = 100, Stage = "Done" }, ct);

        return Result.Success(/* ... */);
    }

}

Same command handler runs from HTTP, SignalR, or WebSocket — the seam unifies them. The HTTP caller gets only the return value; the SignalR / WebSocket caller gets the progress stream and the return value.

When to use which

You're writing Use Why
Code inside a WebSocketHandler (any lifecycle hook or handler-owned background task) this.SendAsync(...) You have direct access to Connection. No need to re-resolve through ambient state. Works regardless of ExecutionContext flow.
A Conductor command/query handler that may be invoked from HTTP, SignalR, or WebSocket IConnectionSender The handler doesn't know its transport. The ambient accessor is the seam.

The two complement perfectly for the AI tool-call story: voice-frame forwarding stays on this.SendAsync(...) (handler-bound, hot path); AI tool calls dispatched through Conductor reach back via IConnectionSender for progress streaming, under the same authenticated identity that the WebSocket was opened with.

What neither API does

Both APIs push only to the currently-executing connection. Broadcast / target-by-ConnectionId / target-by-group is not built into raw WebSocket today — see BACKLOG "Connection registry / fan-out push." For those patterns, use SignalR (Cirreum.Runtime.Invocation.SignalR) — it has them natively via IHubContext<THub>.Clients.X.SendAsync(...). Raw WebSocket is appropriate for streaming pipelines where each connection is independent (voice, telemetry); SignalR is appropriate when broadcast/group/presence are core requirements.

Connection lifecycle

Implement IConnectionLifecycle (from Cirreum.Invocation.Connections) and register it in DI to receive cross-cutting OnConnectedAsync / OnDisconnectedAsync callbacks across all WebSocket connections (and other long-lived sources). The orchestrator dispatches both under a synthetic invocation scope so consumers like IUserStateAccessor work normally inside the callbacks.

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 (orchestrator aborts the WebSocket).
        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: WasGraceful = closeStatus == WebSocketCloseStatus.NormalClosure, Reason = closeStatusDescription, Exception populated when the loop exited due to a thrown exception.

The cancellationToken on OnDisconnectedAsync is a bounded cleanup budget — fires on either the configured DisconnectTimeoutSeconds (default 30 s) or IHostApplicationLifetime.ApplicationStopping, whichever comes first. Pass it directly into cancellable cleanup calls.

Connection termination from the handler

For multi-socket orchestration patterns (Twilio + AI socket; client + downstream worker), the handler can terminate the inbound WebSocket from inside any lifecycle hook by calling Connection.Abort():

public override async Task OnConnectedAsync(CancellationToken ct) {
    _aiSocket = await ConnectToAiAsync(ct);

    _aiTask = Task.Run(async () => {
        try {
            await ReadFromAiAsync(_aiSocket, ct);
        } finally {
            Connection!.Abort();   // ← terminate inbound when AI side ends
        }
    }, ct);
}

Abort() cancels the linked CTS that the orchestrator's WebSocket.ReceiveAsync is waiting on — the receive throws, the frame loop exits cleanly, and OnDisconnectedAsync runs as if the close was orderly.

Subprotocol negotiation

Override OnSelectSubProtocolAsync to negotiate a WebSocket subprotocol (e.g. wire-format version negotiation, app-specific protocols). Read the requested subprotocols from HttpContext.WebSockets.WebSocketRequestedProtocols and return the chosen value (must be one of the offered values, or null for no subprotocol):

public override Task<string?> OnSelectSubProtocolAsync(HttpContext context) {
    var requested = context.WebSockets.WebSocketRequestedProtocols;
    return Task.FromResult(
        requested.Contains("cirreum-v2") ? "cirreum-v2"
      : requested.Contains("cirreum-v1") ? "cirreum-v1"
      : null);
}

After accept, the negotiated value is exposed via Connection.SubProtocol (read-only) for the remainder of the connection's lifetime.

Dependencies

  • Cirreum.Runtime.InvocationProvider 1.1.0+ — L4 helper (IInvocationBuilder scope object, RegisterInvocationProvider<> helper, InvocationProviderMapping record)
  • Cirreum.Invocation.WebSockets 1.0.0+ — L3 registrar, orchestrator, connection adapter, IConnectionSender impl, IWebSocketUrlBuilder, WebSocketHandler base
  • Microsoft.AspNetCore.App (framework reference) — WebSocket (Microsoft.AspNetCore.WebSockets), endpoint routing, hosting

Versioning

Follows Semantic Versioning. Major bumps are coordinated with the L3 Cirreum.Invocation.WebSockets 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.WebSockets:

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 107 5/11/2026
1.1.0 110 5/10/2026
1.0.0 103 5/10/2026