Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

dotnet-websockets-aspnetcore

Build real-time, bidirectional WebSocket services in ASP.NET Core β€” with a thread-safe connection manager, echo + broadcast messaging, and a REST companion API for server-initiated pushes.

Visit CodingDroplets YouTube Patreon Buy Me a Coffee GitHub


πŸš€ Support the Channel β€” Join on Patreon

If this sample saved you time, consider joining our Patreon community. You'll get exclusive .NET tutorials, premium code samples, and early access to new content β€” all for the price of a coffee.

πŸ‘‰ Join CodingDroplets on Patreon

Prefer a one-time tip? Buy us a coffee β˜•


🎯 What You'll Learn

  • How to enable and configure WebSockets in ASP.NET Core with UseWebSockets()
  • How to accept WebSocket upgrade requests using context.WebSockets.AcceptWebSocketAsync()
  • How to implement a thread-safe connection manager with ConcurrentDictionary
  • How to echo messages back to the sender and broadcast to all connected clients
  • How to detect and remove dead connections during broadcast
  • How to build a REST companion API for server-initiated WebSocket pushes
  • When to use WebSockets vs SignalR (and why SignalR is often the better choice)
  • How to unit-test the connection manager without a real network (using WebSocket stubs)
  • How to integration-test the REST endpoints with WebApplicationFactory

πŸ—ΊοΈ Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  ASP.NET Core Application                          β”‚
β”‚                                                                    β”‚
β”‚  HTTP/REST                           WebSocket (ws://)             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ ConnectionsControllerβ”‚           β”‚ /ws Map endpoint          β”‚  β”‚
β”‚  β”‚                      β”‚           β”‚                          β”‚  β”‚
β”‚  β”‚ GET  /api/connections β”‚           β”‚ AcceptWebSocketAsync()   β”‚  β”‚
β”‚  β”‚ POST /api/connections β”‚           β”‚        β”‚                 β”‚  β”‚
β”‚  β”‚      /broadcast       β”‚           β”‚        β–Ό                 β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚  WebSocketHandler        β”‚  β”‚
β”‚               β”‚                     β”‚  (receive loop)          β”‚  β”‚
β”‚               β”‚                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚               β”‚                                  β”‚                β”‚
β”‚               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚
β”‚                                  β–Ό                                β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”‚
β”‚                    β”‚  WebSocketConnectionManager  β”‚               β”‚
β”‚                    β”‚  ConcurrentDictionary        β”‚               β”‚
β”‚                    β”‚  connectionId β†’ WebSocket    β”‚               β”‚
β”‚                    β”‚                             β”‚                β”‚
β”‚                    β”‚  AddSocket()   β†’ id          β”‚               β”‚
β”‚                    β”‚  BroadcastAsync() β†’ all      β”‚               β”‚
β”‚                    β”‚  SendToAsync() β†’ one         β”‚               β”‚
β”‚                    β”‚  RemoveSocket() β†’ cleanup    β”‚               β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

WebSocket Message Flow:
  Client sends text β†’ WebSocketHandler receives β†’ echo back to sender
                                                β†’ broadcast to all others

πŸ“‹ WebSocket vs SignalR β€” Decision Guide

Feature Raw WebSockets (this sample) SignalR
Complexity Low β€” you own everything Medium β€” abstracted
Protocol WebSocket (RFC 6455) only WebSocket, SSE, long-polling
Typed hub methods ❌ Manual JSON parsing βœ… Strongly-typed hubs
Groups / user targeting ❌ Manual tracking βœ… Built-in groups & users
Reconnection handling ❌ Manual βœ… Automatic
Backplane (scale-out) ❌ Manual Redis/custom βœ… Azure SignalR / Redis
Best for IoT, binary streams, proxies Chat, live dashboards, games

Rule of thumb: Use raw WebSockets when you need full protocol control (binary data, custom framing, proxying). Use SignalR for everything else.


πŸ“ Project Structure

dotnet-websockets-aspnetcore/
β”‚
β”œβ”€β”€ dotnet-websockets-aspnetcore.sln        # Solution file
β”‚
β”œβ”€β”€ WebSocketsDemo.Api/                     # Main Web API project
β”‚   β”œβ”€β”€ Controllers/
β”‚   β”‚   └── ConnectionsController.cs        # REST: list connections + server broadcast
β”‚   β”œβ”€β”€ WebSockets/
β”‚   β”‚   β”œβ”€β”€ WebSocketConnectionManager.cs   # Thread-safe connection pool
β”‚   β”‚   └── WebSocketHandler.cs             # Per-connection receive/echo/broadcast loop
β”‚   β”œβ”€β”€ Models/
β”‚   β”‚   └── ConnectionInfo.cs               # Response DTOs
β”‚   β”œβ”€β”€ Properties/
β”‚   β”‚   └── launchSettings.json             # Opens Swagger UI automatically
β”‚   └── Program.cs                          # Middleware pipeline + /ws endpoint
β”‚
└── WebSocketsDemo.Tests/                   # xUnit test project
    β”œβ”€β”€ ConnectionManagerTests.cs           # Unit tests (9 tests, stubs WebSocket)
    └── ConnectionsApiTests.cs              # Integration tests (4 tests, WebApplicationFactory)

πŸ› οΈ Prerequisites

Tool Version Download
.NET SDK 10.0+ https://dotnet.microsoft.com/download
websocat (optional) Any cargo install websocat or GitHub releases
IDE VS 2022 / Rider / VS Code Any will work

⚑ Quick Start

# 1. Clone the repository
git clone https://github.com/codingdroplets/dotnet-websockets-aspnetcore.git
cd dotnet-websockets-aspnetcore

# 2. Build
dotnet build -c Release

# 3. Run the API
cd WebSocketsDemo.Api
dotnet run

# 4. Open Swagger UI (Visual Studio opens automatically)
http://localhost:5289/swagger

# 5. Connect a WebSocket client
websocat ws://localhost:5289/ws

# 6. Type a message and press Enter β€” it will echo back and broadcast to all connected clients
Hello, WebSockets!

πŸ”§ How It Works

Step 1 β€” Enable WebSocket middleware in Program.cs

// IMPORTANT: Must be called before UseRouting() / MapControllers()
app.UseWebSockets(new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromSeconds(30), // Ping to detect dead connections
});

Step 2 β€” Accept WebSocket upgrades at the /ws endpoint

app.Map("/ws", async (HttpContext context) =>
{
    if (!context.WebSockets.IsWebSocketRequest)
    {
        context.Response.StatusCode = 400;
        return;
    }

    var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var connectionId = manager.AddSocket(webSocket);

    // HandleAsync blocks until the client disconnects
    await handler.HandleAsync(connectionId, webSocket, context.RequestAborted);
});

Step 3 β€” Thread-safe connection pool

public sealed class WebSocketConnectionManager
{
    private readonly ConcurrentDictionary<string, WebSocket> _sockets = new();

    public string AddSocket(WebSocket socket)
    {
        var id = Guid.NewGuid().ToString("N");
        _sockets.TryAdd(id, socket);
        return id;
    }

    public async Task BroadcastAsync(string message, CancellationToken ct = default)
    {
        var buffer = Encoding.UTF8.GetBytes(message);
        foreach (var (id, socket) in _sockets)
        {
            if (socket.State == WebSocketState.Open)
                await socket.SendAsync(buffer, WebSocketMessageType.Text, true, ct);
        }
    }
}

Step 4 β€” Message receive loop

var buffer = new byte[4096];
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), ct);

while (!result.CloseStatus.HasValue)
{
    var text = Encoding.UTF8.GetString(buffer, 0, result.Count);
    await manager.SendToAsync(connectionId, JsonSerializer.Serialize(new { type="echo", message=text }), ct);
    await manager.BroadcastAsync(JsonSerializer.Serialize(new { type="message", from=connectionId[..8], message=text }), ct);
    result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), ct);
}

await webSocket.CloseAsync(result.CloseStatus!.Value, result.CloseStatusDescription, ct);

πŸ“‘ API Endpoints

Protocol Method Endpoint Description Success Error
WebSocket β€” ws://host/ws Connect a WebSocket client 101 Switching Protocols β€”
HTTP GET /api/connections List active WebSocket connections 200 OK β€”
HTTP POST /api/connections/broadcast Broadcast message to all WS clients 200 OK 400

Example WebSocket Message Formats (JSON)

System message (join/leave):

{ "type": "system", "message": "Client a1b2c3d4 joined.", "timestamp": "2026-06-13T06:00:00Z" }

Echo (back to sender):

{ "type": "echo", "from": "a1b2c3d4", "message": "Hello!", "timestamp": "2026-06-13T06:00:01Z" }

Broadcast (to all clients):

{ "type": "message", "from": "a1b2c3d4", "message": "Hello!", "timestamp": "2026-06-13T06:00:01Z" }

Server-initiated broadcast (via POST /api/connections/broadcast):

{ "type": "server-broadcast", "message": "Server is restarting in 5 minutes.", "timestamp": "2026-06-13T06:00:00Z" }

πŸ§ͺ Running Tests

dotnet test -c Release
Test Type Verifies
AddSocket_ReturnsUniqueConnectionId Unit Each AddSocket generates a unique ID
AddSocket_IncreasesCount Unit Count increments on add
RemoveSocket_DecreasesCount Unit Count decrements on remove
RemoveSocket_UnknownId_ReturnsNull Unit Graceful handling of unknown IDs
GetSocket_ReturnsRegisteredSocket Unit Lookup returns the exact same instance
GetSocket_UnknownId_ReturnsNull Unit Returns null for missing connections
ConnectionIds_ReturnsAllRegisteredIds Unit All IDs are enumerable
SendToAsync_UnknownId_ReturnsFalse Unit Returns false for missing connection
SendToAsync_ClosedSocket_ReturnsFalse Unit Skips sending to closed socket
GetConnections_Returns200_WithEmptyPool Integration REST endpoint returns correct schema
Broadcast_EmptyMessage_Returns400 Integration Validates empty message input
Broadcast_ValidMessage_Returns200_WithZeroRecipients Integration Server broadcast succeeds with no clients
WsEndpoint_PlainHttpRequest_Returns400 Integration Non-WebSocket request rejected correctly

Result: 13/13 passing βœ…


πŸ€” Key Concepts

Why ConcurrentDictionary?

WebSocket connections arrive from multiple concurrent HTTP requests on different threads. Using a regular Dictionary without synchronisation would cause data races. ConcurrentDictionary is the right default β€” internally partitioned for low-contention concurrent access.

Why a REST companion API?

Sometimes you need to push a message from a background job, a Hangfire task, or another HTTP request β€” not from a WebSocket client. The POST /api/connections/broadcast endpoint lets any server-side code send WebSocket messages without needing to hold an active socket.

Why KeepAliveInterval?

TCP connections can silently die (NAT timeout, proxy drops, client crashes) without sending a WebSocket Close frame. KeepAliveInterval tells ASP.NET Core to send WebSocket ping frames every N seconds. If the client doesn't respond, the connection is closed and cleaned up.

The AddSocket before HandleAsync pattern

Notice: manager.AddSocket(webSocket) is called in Program.cs (before HandleAsync), and HandleAsync is given the already-registered connection ID. This ensures the socket is in the pool before any messages start flowing, which prevents a race where a broadcast fires before a new connection is fully registered.


🏷️ Technologies Used

  • ASP.NET Core 10 β€” Web API + WebSocket middleware
  • IWebSocketManager β€” Built-in WebSocket upgrade support
  • ConcurrentDictionary β€” Thread-safe connection pool
  • Swashbuckle / Swagger UI β€” REST API documentation
  • xUnit β€” Unit and integration test framework
  • WebApplicationFactory β€” Integration test host

πŸ“š References


πŸ“„ License

This project is licensed under the MIT License.


πŸ”— Connect with CodingDroplets

Platform Link
🌐 Website https://codingdroplets.com/
πŸ“Ί YouTube https://www.youtube.com/@CodingDroplets
🎁 Patreon https://www.patreon.com/CodingDroplets
β˜• Buy Me a Coffee https://buymeacoffee.com/codingdroplets
πŸ’» GitHub http://github.com/codingdroplets/

Want more samples like this? Support us on Patreon or buy us a coffee β˜• β€” every bit helps keep the content coming!

About

WebSockets in ASP.NET Core: thread-safe connection manager, echo and broadcast messaging, REST companion API for server-initiated pushes, and 13 unit + integration tests.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages