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.Core
Step 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.js
Use 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"
};
}
}
type Request = { a: number, b: number }
type Response = {
result: number,
operation: string
}
const result = await client.send<Request, Response>('calculator/add', { a: 2, b: 2 })
console.log(`${result.operation}: ${result.result}`)
This gives you HTTP-like request/response semantics over a persistent connection.
Real-World Features
Dependency Injection
Your endpoints are first-class citizens in the DI container:
[WsEndpoint("auth/validate", ServiceLifetime.Singleton)]
public class AuthEndpoint : WsEndpoint<AuthRequest, AuthResponse>
{
private readonly IAuthService _authService;
private readonly ILogger<AuthEndpoint> _logger;
public AuthEndpoint(IAuthService authService, ILogger<AuthEndpoint> logger)
{
_authService = authService;
_logger = logger;
}
public override async Task<AuthResponse> HandleAsync(AuthRequest request, EndpointContext context)
{
var isValid = await _authService.ValidateAsync(request.Token);
return new AuthResponse { Success = isValid };
}
}
Control the lifetime per endpoint. Inject services. Log everything.
Group Management
Building a chat room, live dashboard, or collaborative tool? Groups are built in:
[WsEndpoint("chat/message/send")]
public class ChatEndpoint : WsEndpoint<ChatMessage>
{
public override async Task HandleAsync(ChatMessage request, EndpointContext context)
{
// Join a room
await context.Groups.AddAsync("chat-room", context.ConnectionId);
// Send to one group
await context.Send
.Group("chat-room")
.SendAsync("chat/message/receive", new { Message = "Welcome!" });
// Send to multiple groups at once
await context.Send
.Group("chat-room")
.Group("announcements")
.SendAsync("chat/message/receive", new { Message = "Important update!" });
// Leave a room
await context.Groups.RemoveAsync("chat-room", context.ConnectionId);
}
}
No need to build your own connection registry. It’s handled for you.
Authorization
Apply authorization globally, per-endpoint, or both:
Global (applies to all endpoints under the path):
app.MapEndpoints("/ws", (opt) =>
{
opt.AuthorizationData =
[ new AuthorizeAttribute
{
AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme
}
];
});
Per-endpoint:
[Authorize(Policy = "ws.chat")]
[WsEndpoint("chat/message/send")]
public class ChatEndpoint : WsEndpoint<ChatMessage>
{
//...
}
Global rules for connection-wide concerns, endpoint attributes for fine-grained policies. They compose naturally.
Scaling with Redis
When you need to scale beyond a single server, WebSockets.Otp has you covered:
dotnet add package WebSockets.Otp.Redis
Setup is two lines:
builder.Services.AddSingleton<IConnectionMultiplexer>(
_ => ConnectionMultiplexer.Connect("localhost:6379"));
builder.Services.AddRedisManager();
Now your connection registry is shared across instances. A message sent to a group on Server A reaches clients connected to Server B.
What’s on the Roadmap?
The library is actively evolving. Coming soon:
- Performance optimizations
- Client reconnection handling
- More protocols support
- Pre/Post processors
- Rate limiting
- Versioning
Links:
- GitHub: https://github.com/VladUl287/WebSoket.Otp
- NuGet: https://www.nuget.org/packages/WebSockets.Otp.Core
- npm: https://www.npmjs.com/package/websockets.otp.js
Have questions or feedback? Drop a comment below or open an issue on GitHub. If you find WebSockets.Otp useful, a ⭐ on GitHub!
Top comments (0)