WebSockets.Otp is inspired by REPR (Request-Endpoint-Response) principles and trying to bring the same elegance from Minimal APIs, FastEndpoints, or MediatR to real-time communication.
Let me show you how to build a real-time chat app in under 10 minutes.
Getting Started
Step 1: Install the Package
dotnet add package WebSockets.Otp.CoreStep 2: Define Your First Endpoint
The magic of WebSockets.Otp is how naturally endpoints map to message handlers:
[WsEndpoint("chat/message/send")]
public class ChatEndpoint : WsEndpoint<ChatMessage>
{
public override async Task HandleAsync(ChatMessage message, EndpointContext context)
{
await context.Send
.Group("general-chat")
.SendAsync("chat/message/receive", message, default);
}
}
public class ChatMessage
{
public string Username { get; set; }
public string Message { get; set; }
}That’s it. No base classes to wrestle with, no complex configuration. Just a class with a HandleAsync method.
Step 3: Wire It Up
In your Program.cs:
builder.Services.AddWsEndpoints();
app.MapEndpoints("/ws", (opt) =>
{
opt.OnConnected = async (context) =>
{
await context.Groups.AddAsync("general-chat", context.ConnectionId);
};
opt.OnDisconnected = async (context) =>
{
await context.Groups.RemoveAsync("general-chat", context.ConnectionId);
};
})Three lines of configuration. Your WebSocket server is ready.
The Client Side
Install the JavaScript client:
npm i websockets.otp.jsUse it:
const client = useWsClient({
url: "ws://localhost:5096/ws"
});
await client.connect();
type Message = { username: string, message: string };
// Listen for messages
const off = client.receive<Message>('chat/message/receive', (msg) => {
console.log(`${msg.username}: ${msg.message}`);
});
// Send a message
await client.notify<Message>('chat/message/send', {
username: "user1",
message: "Hello, WebSockets.Otp!"
});The client mirrors the server’s endpoint-based approach. Your message names are your routes.
Three Endpoint Patterns
WebSockets.Otp supports three patterns, each for different use cases:
1. Fire-and-Forget (No Request/Response)
Perfect for telemetry, logging, or one-way notifications:
[WsEndpoint("system/status")]
public class SystemStatusEndpoint : WsEndpoint
{
public override async Task HandleAsync(EndpointContext context)
{
var buffer = context.Payload.Span;
// Process raw data if send any
}
}2. Request-Only
When you need to receive data but don’t need to respond directly:
[WsEndpoint("notifications/subscribe")]
public class SubscribeEndpoint : WsEndpoint<SubscribeRequest>
{
public override async Task HandleAsync(SubscribeRequest request, EndpointContext context)
{
await context.Groups.AddAsync("notifications", context.ConnectionId);
await context.Send.SendAsync("notifications/subscribed", new
{
ConnectionId = context.ConnectionId,
Data = "Subscribed successfully"
}, default);
}
}3. Request/Response
When you need a direct reply — like an RPC call over WebSockets:
[WsEndpoint("calculator/add")]
public class AddEndpoint : WsEndpoint<AddRequest, AddResponse>
{
public override async Task<AddResponse> HandleAsync(AddRequest request, EndpointContext context)
{
return new AddResponse
{
Result = request.A + request.B,
Operation = "addition"
};
}
}
Join the conversation! Your thoughts help the community grow.