OddSockets C# SDK
Official C#/.NET SDK for OddSockets real-time messaging platform
Overview & Features
The OddSockets C# SDK provides a powerful, type-safe interface for real-time messaging in .NET applications, supporting both server-side and client-side scenarios.
.NET 6+ Support
Full support for modern .NET with async/await patterns and nullable reference types.
Type Safety
Comprehensive type definitions with compile-time safety and IntelliSense support.
Cross-Platform
Works on Windows, macOS, and Linux with .NET Core/5+.
High Performance
Optimized for low latency with efficient WebSocket connections and smart routing.
Cost Effective
No per-message pricing, industry-standard 32KB message limits, transparent pricing.
Automatic Failover
Built-in redundancy and intelligent error handling for 99.9% uptime.
Installation
Install-Package OddSockets.DotNet.SDK
dotnet add package OddSockets.DotNet.SDK
<PackageReference Include="OddSockets.DotNet.SDK" Version="0.1.0-beta.1" />
Quick Start
Basic Usage
using OddSockets;
using OddSockets.Models;
var config = new OddSocketsConfig
{
ApiKey = "ak_live_1234567890abcdef"
};
var client = new OddSocketsClient(config);
// Connect to OddSockets
await client.ConnectAsync();
var channel = client.Channel("my-channel");
// Subscribe to messages
await channel.SubscribeAsync(message =>
{
Console.WriteLine($"Received: {message.Data}");
});
// Publish a message
await channel.PublishAsync("Hello, World!");
Dependency Injection
// Program.cs or Startup.cs
services.AddSingleton(new OddSocketsConfig
{
ApiKey = "ak_live_1234567890abcdef",
AutoConnect = true
});
services.AddSingleton<OddSocketsClient>();
// In your service
public class ChatService
{
private readonly OddSocketsClient _client;
public ChatService(OddSocketsClient client)
{
_client = client;
}
public async Task SendMessageAsync(string channelName, string message)
{
var channel = _client.Channel(channelName);
await channel.PublishAsync(message);
}
}
ASP.NET Core Integration
[ApiController]
[Route("api/[controller]")]
public class ChatController : ControllerBase
{
private readonly OddSocketsClient _client;
public ChatController(OddSocketsClient client)
{
_client = client;
}
[HttpPost("send")]
public async Task<IActionResult> SendMessage([FromBody] SendMessageRequest request)
{
var channel = _client.Channel(request.ChannelName);
var result = await channel.PublishAsync(request.Message);
return Ok(result);
}
}
Configuration
Client Options
var config = new OddSocketsConfig
{
ApiKey = "your-api-key", // Required: Your OddSockets API key
UserId = "user-id", // Optional: User identifier
AutoConnect = true, // Optional: Auto-connect on creation
ReconnectAttempts = 5, // Optional: Max reconnection attempts
HeartbeatInterval = 30000, // Optional: Heartbeat interval (ms)
Timeout = 10 // Optional: Connection timeout (seconds)
};
var client = new OddSocketsClient(config);
Channel Options
// Subscribe with options
await channel.SubscribeAsync(callback, new SubscribeOptions
{
EnablePresence = true, // Enable presence tracking
RetainHistory = true, // Retain message history
FilterExpression = "user.premium == true" // Message filter expression
});
// Publish with options
await channel.PublishAsync(message, new PublishOptions
{
Ttl = 3600, // Time to live (seconds)
Metadata = new { Priority = "high" }, // Additional metadata
StoreInHistory = true // Store in message history
});
Examples
Explore comprehensive examples demonstrating the OddSockets C# SDK in action:
Enhanced Features
Beyond core pub/sub, OddSockets ships a Slack-like enhanced surface —
reactions, typing indicators, threads, read receipts, presence/status, notifications, DMs,
channel management, message editing and search. It lives on client.Enhanced.
Send an action with a client.Enhanced.*Async(...) method
(PascalCase, positional arguments); receive the paired broadcast with
client.On("<event>", data => ...). The worker forwards every enhanced
broadcast onto the client's raw event surface, delivered as a
System.Text.Json.JsonElement.
Typing & Reactions
using System.Text.Json;
var channel = client.Channel("room-42");
await channel.SubscribeAsync(message => { /* ... */ },
SubscribeOptions.Builder().WithPresence(true).Build());
// Receive-path: broadcasts from other users on the channel
client.On("user_typing", data => Console.WriteLine($"{data.GetProperty("userId")} is typing"));
client.On("reaction_added", data => Console.WriteLine($"reaction {data.GetProperty("emoji")}"));
// Send-path: enhanced actions over the live socket
await client.Enhanced.StartTypingAsync("alice", "room-42");
await client.Enhanced.AddReactionAsync("msg-1", "room-42", ":thumbsup:", "alice", "Alice");
Threads
client.On("thread_reply", data => Console.WriteLine("new thread reply"));
// ThreadReplyAsync returns Task<JsonElement> with the worker's ack
var reply = await client.Enhanced.ThreadReplyAsync(
"room-42", "msg-1", "Replying in the thread", "alice", "Alice");
var thread = await client.Enhanced.GetThreadAsync("thread-123");
Each area exposes methods on client.Enhanced; the worker broadcasts the paired
events which you handle with client.On(...). Query methods
(Get*Async, Search*Async) return a Task<JsonElement>
that resolves with the worker response.
- Typing —
StartTypingAsync,StopTypingAsync→user_typing,user_stopped_typing - Reactions —
AddReactionAsync,RemoveReactionAsync,GetReactionsAsync→reaction_added,reaction_removed - Threads —
ThreadReplyAsync,GetThreadAsync,SubscribeThreadAsync,FollowThreadAsync,UnfollowThreadAsync,MarkThreadReadAsync→thread_reply,thread_subscribed,thread_followed,thread_read_updated - Read receipts —
MarkReadAsync,MarkAllReadAsync,GetUnreadCountsAsync→user_read,unread_count_updated,all_marked_read - Messages —
EditMessageAsync,DeleteMessageAsync,PinMessageAsync,UnpinMessageAsync,GetPinnedMessagesAsync→message_edited,message_deleted,message_pinned,message_unpinned - Presence & status —
SetStatusAsync,SetCustomStatusAsync,ClearCustomStatusAsync,SetDNDAsync,ClearDNDAsync,GetUserPresenceAsync→user_status_changed,custom_status_updated,dnd_status_changed - Channels —
CreateChannelAsync,UpdateChannelAsync,ArchiveChannelAsync,InviteToChannelAsync,JoinChannelAsync,LeaveChannelAsync,GetChannelMembersAsync→channel_created,channel_updated,user_invited,user_joined_channel,user_left_channel - DMs —
CreateDMAsync,SendDMAsync,GetDMConversationsAsync→dm_created,dm_received - Notifications —
SubscribeNotificationsAsync,GetNotificationsAsync,MarkNotificationReadAsync,ClearNotificationsAsync→notification,notification_read,notifications_cleared - Search —
SearchMessagesAsync,SearchInChannelAsync,SearchByUserAsync,FilterMessagesAsync→ results returned viaTask<JsonElement>
For any worker event not wrapped above, subscribe with the raw
client.On("<event>", handler) API — all enhanced broadcasts are
forwarded onto the client surface.
Challenges & Leaderboards
Challenges, leaderboards and achievements build on the same live socket. The
send side lives on the enhanced surface (client.Enhanced);
request/query methods resolve with the worker's reply, while progress and achievement
calls are fire-and-forget. Inbound broadcasts arrive on the client event surface —
subscribe with the client's normal client.On("<event>", data => ...),
delivered as a System.Text.Json.JsonElement.
// Create a ranked challenge, report progress, read standings, then finalize
await client.Enhanced.CreateChallengeAsync("weekly-sprint", "score", ranked: true);
await client.Enhanced.ReportProgressAsync("weekly-sprint", 1200);
var standings = await client.Enhanced.GetStandingsAsync("weekly-sprint", limit: 10);
var result = await client.Enhanced.CompleteChallengeAsync("weekly-sprint", "completed");
Query and lifecycle methods return a Task<JsonElement> that resolves
with the worker's ack; ReportProgressAsync and
UnlockAchievementAsync are fire-and-forget and return a plain
Task.
- CreateChallengeAsync — create a challenge/leaderboard → ack
challenge_create_success - ReportProgressAsync — fire-and-forget metric progress (no ack)
- CompleteChallengeAsync — finalize with an outcome → ack
challenge_complete_success - UnlockAchievementAsync — fire-and-forget; pass
percentComplete(0–100) (no ack) - GetStandingsAsync — request top-N + caller rank → ack
challenge_standings_success - GetAchievementsAsync — query achievement state → ack
achievement_state - SendChallengeInviteAsync — directed invite to another user → ack
challenge_invite_success - ReplyChallengeInviteAsync — accept/decline an invite → ack
challenge_reply_success - CancelChallengeInviteAsync — cancel a sent invite → ack
challenge_invite_cancel_success - GetChallengeInvitesAsync — list pending invites → ack
challenge_invites
Outcome vocabulary
Pass one of these as the outcome for CompleteChallengeAsync:
completed— win (rank 1)failed— losstied— drawconceded— resign / concedeexpired— timed out
Progressive achievements
UnlockAchievementAsync always emits the wire event
achievement_unlock; the worker is authoritative and derives the outbound
broadcast from percentComplete: a value < 100 broadcasts
achievement_progress (status in_progress), while
>= 100 or an omitted value broadcasts achievement_unlock
(status unlocked).
Inbound events
Subscribe with client.On(...) to receive these broadcasts:
- Room broadcasts —
challenge_progress,leaderboard_rank_change,challenge_complete,achievement_unlock,achievement_progress - Directed (per-user) —
challenge_invited,challenge_reply_received,challenge_invite_cancelled
Usage Analytics
Pull your tenant's headline usage tiles — monthly active users, daily active users, total messages published, and error rate — straight from the SDK, without hand-rolling a REST call. GetUsageStatsAsync() resolves the same four tiles the developer dashboard renders.
var stats = await client.GetUsageStatsAsync();
// Nullable long?/double? tiles stay null when a leg is not live yet.
string Tile(object v) => v?.ToString() ?? "\u2014"; // em-dash for null
Console.WriteLine($"MAU: {Tile(stats.Mau)}");
Console.WriteLine($"DAU: {Tile(stats.Dau)}");
Console.WriteLine($"Total messages: {Tile(stats.TotalMessages)}");
Console.WriteLine($"Error rate: {Tile(stats.ErrorRate)}");
The four tiles
- Mau — monthly active users for your owner scope
- Dau — daily active users
- TotalMessages — total messages published
- ErrorRate — publish error rate,
0–1
Honesty rule — null, never a fake zero
Each tile is a nullable number (long? / double?). A null means that leg of the analytics pipeline is not live yet for your tenant — the SDK returns it verbatim and never coerces it to 0. Render an em-dash (—) for a null tile so you never show a fabricated zero.
Requires an API key
GetUsageStatsAsync() reads your owner-scoped analytics, so it needs an apiKey. Keyless / token-only clients have no owner scope to query and will throw getUsageStats requires an apiKey.
Performance & Compatibility
OddSockets C# SDK delivers superior performance with broad .NET compatibility:
.NET Support
- .NET 6+ (LTS)
- .NET Core 3.1+
- .NET Framework 4.8+
- C# 10+ features
Platform Support
- Windows (x64, ARM64)
- macOS (Intel, Apple Silicon)
- Linux (x64, ARM64)
- Docker containers
Framework Integrations
The OddSockets C# SDK works seamlessly with all .NET frameworks and platforms. Here are examples showing how to integrate with popular .NET technologies:
ASP.NET Core Web API
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(new OddSocketsConfig
{
ApiKey = builder.Configuration["OddSockets:ApiKey"]!,
AutoConnect = true
});
builder.Services.AddSingleton<OddSocketsClient>();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
// Controllers/ChatController.cs
[ApiController]
[Route("api/[controller]")]
public class ChatController : ControllerBase
{
private readonly OddSocketsClient _client;
public ChatController(OddSocketsClient client)
{
_client = client;
}
[HttpPost("channels/{channelName}/messages")]
public async Task<IActionResult> SendMessage(string channelName, [FromBody] object message)
{
var channel = _client.Channel(channelName);
var result = await channel.PublishAsync(message);
return Ok(result);
}
[HttpGet("channels/{channelName}/history")]
public async Task<IActionResult> GetHistory(string channelName, [FromQuery] int limit = 100)
{
var channel = _client.Channel(channelName);
var history = await channel.GetHistoryAsync(new HistoryOptions { Limit = limit });
return Ok(history);
}
}
Blazor Server
@page "/chat"
@using OddSockets
@using OddSockets.Models
@inject OddSocketsClient Client
@implements IDisposable
<h3>Real-time Chat</h3>
<div class="chat-messages">
@foreach (var message in messages)
{
<div class="message">
<strong>@message.UserId:</strong> @message.Data
</div>
}
</div>
<div class="chat-input">
<input @bind="currentMessage" @onkeypress="HandleKeyPress" placeholder="Type a message..." />
<button @onclick="SendMessage">Send</button>
</div>
@code {
private List<Message> messages = new();
private string currentMessage = "";
private OddSocketsChannel? channel;
protected override async Task OnInitializedAsync()
{
channel = Client.Channel("blazor-chat");
await channel.SubscribeAsync(message =>
{
messages.Add(message);
InvokeAsync(StateHasChanged);
});
}
private async Task SendMessage()
{
if (!string.IsNullOrWhiteSpace(currentMessage) && channel != null)
{
await channel.PublishAsync(currentMessage);
currentMessage = "";
}
}
private async Task HandleKeyPress(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
await SendMessage();
}
}
public void Dispose()
{
channel?.UnsubscribeAsync();
}
}
Worker Service
public class ChatWorkerService : BackgroundService
{
private readonly OddSocketsClient _client;
private readonly ILogger<ChatWorkerService> _logger;
public ChatWorkerService(OddSocketsClient client, ILogger<ChatWorkerService> logger)
{
_client = client;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _client.ConnectAsync(stoppingToken);
var channel = _client.Channel("worker-notifications");
await channel.SubscribeAsync(async message =>
{
_logger.LogInformation("Received notification: {Message}", message.Data);
// Process the notification
await ProcessNotification(message);
}, cancellationToken: stoppingToken);
// Keep the service running
await Task.Delay(Timeout.Infinite, stoppingToken);
}
private async Task ProcessNotification(Message message)
{
// Your notification processing logic here
await Task.CompletedTask;
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
await _client.DisconnectAsync(cancellationToken);
await base.StopAsync(cancellationToken);
}
}
Console Application
using OddSockets;
using OddSockets.Models;
// Configure the client
var config = new OddSocketsConfig
{
ApiKey = "ak_live_1234567890abcdef",
UserId = "console-user"
};
var client = new OddSocketsClient(config);
try
{
// Connect to OddSockets
await client.ConnectAsync();
Console.WriteLine("Connected to OddSockets!");
// Get a channel
var channel = client.Channel("console-chat");
// Subscribe to messages
await channel.SubscribeAsync(message =>
{
Console.WriteLine($"[{message.Timestamp:HH:mm:ss}] {message.UserId}: {message.Data}");
});
Console.WriteLine("Subscribed to console-chat. Type messages (or 'quit' to exit):");
// Read user input and send messages
string? input;
while ((input = Console.ReadLine()) != "quit")
{
if (!string.IsNullOrWhiteSpace(input))
{
await channel.PublishAsync(input);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
await client.DisconnectAsync();
Console.WriteLine("Disconnected from OddSockets.");
}
MAUI (Multi-platform App UI)
// MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddSingleton(new OddSocketsConfig
{
ApiKey = "ak_live_1234567890abcdef",
AutoConnect = true
});
builder.Services.AddSingleton<OddSocketsClient>();
builder.Services.AddSingleton<MainPage>();
return builder.Build();
}
}
// MainPage.xaml.cs
public partial class MainPage : ContentPage
{
private readonly OddSocketsClient _client;
private OddSocketsChannel? _channel;
public MainPage(OddSocketsClient client)
{
InitializeComponent();
_client = client;
}
protected override async void OnAppearing()
{
base.OnAppearing();
_channel = _client.Channel("maui-chat");
await _channel.SubscribeAsync(message =>
{
MainThread.BeginInvokeOnMainThread(() =>
{
// Update UI with new message
DisplayMessage(message);
});
});
}
private void DisplayMessage(Message message)
{
// Update your UI here
MessagesLabel.Text += $"\n{message.UserId}: {message.Data}";
}
private async void OnSendClicked(object sender, EventArgs e)
{
if (_channel != null && !string.IsNullOrWhiteSpace(MessageEntry.Text))
{
await _channel.PublishAsync(MessageEntry.Text);
MessageEntry.Text = "";
}
}
}