RepletoryLib.WebSocket.Core 1.0.0

dotnet add package RepletoryLib.WebSocket.Core --version 1.0.0
                    
NuGet\Install-Package RepletoryLib.WebSocket.Core -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="RepletoryLib.WebSocket.Core" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RepletoryLib.WebSocket.Core" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="RepletoryLib.WebSocket.Core" />
                    
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 RepletoryLib.WebSocket.Core --version 1.0.0
                    
#r "nuget: RepletoryLib.WebSocket.Core, 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 RepletoryLib.WebSocket.Core@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=RepletoryLib.WebSocket.Core&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=RepletoryLib.WebSocket.Core&version=1.0.0
                    
Install as a Cake Tool

RepletoryLib.WebSocket.Core

In-memory WebSocket hub implementation with connection tracking, group management, broadcast, heartbeat monitoring, and ASP.NET Core middleware for RepletoryLib.

Part of the RepletoryLib ecosystem -- standalone, reusable .NET 10 libraries with zero business logic.

NuGet .NET 10 License: MIT


Overview

RepletoryLib.WebSocket.Core provides the default in-memory implementation of the RepletoryLib.WebSocket.Abstractions contracts. It includes InMemoryWebSocketConnectionManager for tracking connections using concurrent dictionaries, WebSocketHub for sending messages and managing groups, WebSocketMiddleware for handling the full WebSocket connection lifecycle (acceptance, receive loop, message routing), and WebSocketHeartbeatService for background keep-alive monitoring and idle connection cleanup.

This package is suitable for single-server deployments. For horizontally scaled environments, consider implementing the abstractions with a distributed backplane (e.g., Redis).

Key Features

  • AddRepletoryWebSocket -- Single-call DI registration for all WebSocket services, options, and middleware
  • UseRepletoryWebSocket -- App builder extension to configure ASP.NET Core WebSocket middleware and map the endpoint
  • InMemoryWebSocketConnectionManager -- Thread-safe connection tracking with user-to-connection and group-to-connection mappings using ConcurrentDictionary
  • WebSocketHub -- Default IWebSocketHub implementation that serializes messages to JSON and sends via raw System.Net.WebSockets.WebSocket
  • WebSocketMiddleware -- Full lifecycle handler: WebSocket acceptance, origin validation, connection limits, receive loop with message deserialization, and handler routing
  • WebSocketHeartbeatService -- Background IHostedService that periodically pings active connections and closes timed-out ones
  • WebSocketCoreOptions -- Core-specific configuration extending WebSocketOptions with buffer size, keep-alive interval, and heartbeat toggle
  • Automatic user association -- Extracts ClaimTypes.NameIdentifier from the authenticated user on connection
  • Message size enforcement -- Rejects oversized messages based on MaxMessageSizeBytes
  • Origin validation -- Blocks connections from non-whitelisted origins when AllowedOrigins is configured

Installation

dotnet add package RepletoryLib.WebSocket.Core

Or add to your .csproj:

<PackageReference Include="RepletoryLib.WebSocket.Core" Version="1.0.0" />

Note: RepletoryLib packages are published to a local BaGet feed. See the main repository README for feed configuration.

Dependencies

Package Type
RepletoryLib.WebSocket.Abstractions RepletoryLib
Microsoft.AspNetCore.App Framework Reference

Quick Start

Complete Program.cs Setup

using RepletoryLib.WebSocket.Core;

var builder = WebApplication.CreateBuilder(args);

// Register WebSocket services (connection manager, hub, heartbeat, middleware)
builder.Services.AddRepletoryWebSocket(builder.Configuration);

// Register your custom message handlers
builder.Services.AddSingleton<IWebSocketMessageHandler, ChatMessageHandler>();
builder.Services.AddSingleton<IWebSocketMessageHandler, PingMessageHandler>();

var app = builder.Build();

// Configure the WebSocket middleware and map the endpoint
app.UseRepletoryWebSocket();

app.Run();

appsettings.json

{
  "WebSocket": {
    "HeartbeatIntervalSeconds": 30,
    "MaxConnections": 10000,
    "MaxMessageSizeBytes": 65536,
    "ConnectionTimeoutSeconds": 120,
    "AllowedOrigins": [],
    "EndpointPath": "/ws",
    "Core": {
      "ReceiveBufferSize": 4096,
      "KeepAliveIntervalSeconds": 30,
      "EnableHeartbeat": true
    }
  }
}

Configuration

WebSocketOptions (base)

Bound from the "WebSocket" configuration section.

Property Type Default Description
HeartbeatIntervalSeconds int 30 Interval in seconds between heartbeat ping/pong messages
MaxConnections int 10000 Maximum number of concurrent WebSocket connections
MaxMessageSizeBytes int 65536 (64 KB) Maximum allowed size for a single WebSocket message
ConnectionTimeoutSeconds int 120 Seconds of inactivity before a connection is closed
AllowedOrigins List<string> [] (all allowed) Allowed origins for WebSocket requests. Empty list permits all
EndpointPath string "/ws" URL path the WebSocket endpoint listens on

WebSocketCoreOptions (extends WebSocketOptions)

Bound from the "WebSocket:Core" configuration section.

Property Type Default Description
ReceiveBufferSize int 4096 Size in bytes of the buffer used for receiving WebSocket messages
KeepAliveIntervalSeconds int 30 Interval in seconds for the ASP.NET Core WebSocket keep-alive mechanism
EnableHeartbeat bool true Whether the background heartbeat service is enabled

Usage Examples

DI Registration

The AddRepletoryWebSocket extension method registers all core services in a single call:

builder.Services.AddRepletoryWebSocket(builder.Configuration);

This registers:

  • WebSocketOptions bound from "WebSocket" section
  • WebSocketCoreOptions bound from "WebSocket:Core" section
  • InMemoryWebSocketConnectionManager as singleton (also exposed as IWebSocketConnectionManager)
  • WebSocketHub as singleton implementing IWebSocketHub
  • WebSocketHeartbeatService as a hosted service (when EnableHeartbeat is true)
  • WebSocketMiddleware as singleton

App Builder Configuration

The UseRepletoryWebSocket extension method configures the middleware pipeline:

app.UseRepletoryWebSocket();

This performs:

  1. Configures the ASP.NET Core WebSocket middleware with the KeepAliveIntervalSeconds from WebSocketCoreOptions
  2. Maps the WebSocket endpoint at the path specified by WebSocketOptions.EndpointPath (defaults to /ws)

Sending Messages from a Controller

using RepletoryLib.WebSocket.Abstractions.Interfaces;
using RepletoryLib.WebSocket.Abstractions.Models;

[ApiController]
[Route("api/[controller]")]
public class NotificationsController : ControllerBase
{
    private readonly IWebSocketHub _hub;

    public NotificationsController(IWebSocketHub hub) => _hub = hub;

    [HttpPost("broadcast")]
    public async Task<IActionResult> Broadcast([FromBody] string message)
    {
        var result = await _hub.BroadcastAsync(new BroadcastRequest
        {
            Message = new WebSocketMessage
            {
                Type = "Announcement",
                Payload = message
            }
        });

        return Ok(new { result.SuccessCount, result.FailureCount });
    }

    [HttpPost("send/{connectionId}")]
    public async Task<IActionResult> SendToConnection(string connectionId, [FromBody] string message)
    {
        var result = await _hub.SendToConnectionAsync(new SendMessageRequest
        {
            ConnectionId = connectionId,
            Message = new WebSocketMessage
            {
                Type = "DirectMessage",
                Payload = message
            }
        });

        return result.Success ? Ok() : NotFound(result.Error);
    }

    [HttpPost("groups/{groupName}/broadcast")]
    public async Task<IActionResult> BroadcastToGroup(string groupName, [FromBody] string message)
    {
        var result = await _hub.BroadcastToGroupAsync(new GroupBroadcastRequest
        {
            GroupName = groupName,
            Message = new WebSocketMessage
            {
                Type = "GroupMessage",
                Payload = message
            }
        });

        return Ok(new { result.SuccessCount, result.FailureCount });
    }
}

Implementing Custom Message Handlers

Register one or more IWebSocketMessageHandler implementations to process incoming messages. The middleware matches incoming messages to handlers by the MessageType property.

using RepletoryLib.WebSocket.Abstractions.Interfaces;
using RepletoryLib.WebSocket.Abstractions.Models;

public class ChatMessageHandler : IWebSocketMessageHandler
{
    public string MessageType => "Chat";

    private readonly IWebSocketHub _hub;

    public ChatMessageHandler(IWebSocketHub hub) => _hub = hub;

    public async Task HandleAsync(WebSocketMessage message, WebSocketConnection connection, CancellationToken cancellationToken = default)
    {
        // Broadcast the chat message to all connections in the sender's groups
        foreach (var group in connection.Groups)
        {
            await _hub.BroadcastToGroupAsync(new GroupBroadcastRequest
            {
                GroupName = group,
                Message = message,
                ExcludeConnectionIds = [connection.ConnectionId]
            }, cancellationToken);
        }
    }
}

public class JoinGroupHandler : IWebSocketMessageHandler
{
    public string MessageType => "JoinGroup";

    private readonly IWebSocketHub _hub;

    public JoinGroupHandler(IWebSocketHub hub) => _hub = hub;

    public async Task HandleAsync(WebSocketMessage message, WebSocketConnection connection, CancellationToken cancellationToken = default)
    {
        var groupName = message.Payload;
        await _hub.AddToGroupAsync(connection.ConnectionId, groupName, cancellationToken);
    }
}

// Register in DI
builder.Services.AddSingleton<IWebSocketMessageHandler, ChatMessageHandler>();
builder.Services.AddSingleton<IWebSocketMessageHandler, JoinGroupHandler>();

Querying Connection State

using RepletoryLib.WebSocket.Abstractions.Interfaces;
using RepletoryLib.WebSocket.Abstractions.Models;

[ApiController]
[Route("api/[controller]")]
public class ConnectionsController : ControllerBase
{
    private readonly IWebSocketConnectionManager _connectionManager;

    public ConnectionsController(IWebSocketConnectionManager connectionManager)
        => _connectionManager = connectionManager;

    [HttpGet("count")]
    public async Task<IActionResult> GetCount()
    {
        var count = await _connectionManager.GetConnectionCountAsync();
        return Ok(new { count });
    }

    [HttpGet("user/{userId}")]
    public async Task<IActionResult> GetByUser(string userId)
    {
        var connections = await _connectionManager.GetConnectionsByUserAsync(userId);
        return Ok(connections.Select(c => new
        {
            c.ConnectionId,
            c.ConnectedAt,
            c.LastActivityAt,
            c.Groups
        }));
    }

    [HttpGet("group/{groupName}")]
    public async Task<IActionResult> GetByGroup(string groupName)
    {
        var connections = await _connectionManager.GetConnectionsByGroupAsync(groupName);
        return Ok(connections.Select(c => new { c.ConnectionId, c.UserId }));
    }
}

Connecting from a JavaScript Client

const ws = new WebSocket('wss://your-server.com/ws');

ws.onopen = () => {
    console.log('Connected');

    // Join a group
    ws.send(JSON.stringify({
        Type: 'JoinGroup',
        Payload: 'room-general'
    }));
};

ws.onmessage = (event) => {
    const message = JSON.parse(event.data);
    console.log(`[${message.Type}] ${message.Payload}`);
};

// Send a chat message
ws.send(JSON.stringify({
    Type: 'Chat',
    Payload: 'Hello, everyone!'
}));

Architecture

Middleware Pipeline

When a request arrives at the configured EndpointPath:

  1. WebSocket validation -- Rejects non-WebSocket requests with 400 Bad Request
  2. Connection limit check -- Returns 503 Service Unavailable if MaxConnections is reached
  3. Origin validation -- Returns 403 Forbidden if the origin is not in AllowedOrigins (when configured)
  4. Connection registration -- Creates a WebSocketConnection and associates the authenticated user (via ClaimTypes.NameIdentifier)
  5. Socket acceptance -- Accepts the WebSocket upgrade and registers the raw socket
  6. Connected notification -- Sends a Connected message back to the client with the assigned connection ID
  7. Receive loop -- Reads messages, enforces size limits, deserializes JSON to WebSocketMessage, and routes to matching IWebSocketMessageHandler
  8. Cleanup -- On disconnect, unregisters the socket, removes the connection, and closes the WebSocket gracefully

Heartbeat Service

When EnableHeartbeat is true, the WebSocketHeartbeatService runs as a background hosted service:

  • Interval -- Ticks every HeartbeatIntervalSeconds
  • Timeout detection -- Connections idle longer than ConnectionTimeoutSeconds are closed and removed
  • Keep-alive -- Active connections receive a JSON ping message ({"Type":"Ping","Payload":""}) and have their LastActivityAt updated

Service Registrations

Service Lifetime Interface
InMemoryWebSocketConnectionManager Singleton IWebSocketConnectionManager
WebSocketHub Singleton IWebSocketHub
WebSocketHeartbeatService Hosted Service --
WebSocketMiddleware Singleton --

Integration with Other RepletoryLib Packages

Package Relationship
RepletoryLib.WebSocket.Abstractions Direct dependency -- interfaces, models, enums, and base options
RepletoryLib.Common Transitive dependency via Abstractions

Testing

using NSubstitute;
using RepletoryLib.WebSocket.Abstractions.Interfaces;
using RepletoryLib.WebSocket.Abstractions.Models;

public class ChatMessageHandlerTests
{
    [Fact]
    public async Task HandleAsync_broadcasts_to_all_groups()
    {
        // Arrange
        var hub = Substitute.For<IWebSocketHub>();
        hub.BroadcastToGroupAsync(Arg.Any<GroupBroadcastRequest>(), Arg.Any<CancellationToken>())
            .Returns(new BulkSendResult());

        var handler = new ChatMessageHandler(hub);

        var connection = new WebSocketConnection
        {
            ConnectionId = "conn-1",
            UserId = "user-1",
            Groups = ["room-general", "room-dev"]
        };

        var message = new WebSocketMessage
        {
            Type = "Chat",
            Payload = "Hello!"
        };

        // Act
        await handler.HandleAsync(message, connection);

        // Assert
        await hub.Received(2).BroadcastToGroupAsync(
            Arg.Is<GroupBroadcastRequest>(r =>
                r.Message == message &&
                r.ExcludeConnectionIds!.Contains("conn-1")),
            Arg.Any<CancellationToken>());
    }
}

Troubleshooting

Issue Solution
400 Bad Request on connect Ensure the client is making a WebSocket upgrade request, not a regular HTTP request
503 Service Unavailable on connect MaxConnections limit reached; increase the limit or scale horizontally
403 Forbidden on connect The client's origin is not in AllowedOrigins; add it to the list or clear the list to allow all origins
Messages not reaching handlers Ensure IWebSocketMessageHandler implementations are registered in DI and the MessageType string exactly matches the incoming message's Type
Connections timing out unexpectedly Increase ConnectionTimeoutSeconds or ensure clients are sending messages frequently enough to keep the connection active
Heartbeat not running Verify EnableHeartbeat is true in the "WebSocket:Core" configuration section
Large messages silently dropped Messages exceeding MaxMessageSizeBytes are logged as warnings and discarded; increase the limit if needed
Anonymous connections (no user ID) The middleware extracts ClaimTypes.NameIdentifier from HttpContext.User; ensure authentication middleware runs before the WebSocket endpoint

License

This project is licensed under the MIT License.

Copyright (c) 2024-2026 Repletory.


For complete documentation, infrastructure setup, and configuration reference, see the RepletoryLib main repository.

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 78 3/2/2026