OddSockets C# SDK

Official C#/.NET SDK for OddSockets real-time messaging platform

.NET 6+ C# 10+ Cross-Platform High Performance Type Safe

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: the cluster reroutes you if a node goes away and the SDK resubscribes automatically.

Installation

Install from source for now. The NuGet package is not published yet, so the commands in the tabs below will not resolve. Use this instead — it pulls the same code the tabs will install once the package is live:
bash
git clone https://github.com/jyswee/oddsockets-csharp-sdk
dotnet add YourProject reference oddsockets-csharp-sdk/OddSockets.csproj
powershell
Install-Package OddSockets.DotNet.SDK
bash
dotnet add package OddSockets.DotNet.SDK
xml
<PackageReference Include="OddSockets.DotNet.SDK" Version="0.1.0-beta.1" />

Quick Start

Basic Usage

csharp
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

csharp
// 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

csharp
[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

csharp
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

csharp
// 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 via Task<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 — loss
  • tied — draw
  • conceded — resign / concede
  • expired — 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

Platform limits and plan ceilings, plus the versions and platforms this SDK supports:

32KB
Max message size
100M
Messages/mo (Scale)
5,000
Peak connections (Scale)
99.999%
Uptime SLA (Enterprise)

.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

csharp
// 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

csharp
@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

csharp
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

csharp
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)

csharp
// 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 = "";
        }
    }
}