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 for 99.9% uptime.

Installation

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.

  • TypingStartTypingAsync, StopTypingAsyncuser_typing, user_stopped_typing
  • ReactionsAddReactionAsync, RemoveReactionAsync, GetReactionsAsyncreaction_added, reaction_removed
  • ThreadsThreadReplyAsync, GetThreadAsync, SubscribeThreadAsync, FollowThreadAsync, UnfollowThreadAsync, MarkThreadReadAsyncthread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receiptsMarkReadAsync, MarkAllReadAsync, GetUnreadCountsAsyncuser_read, unread_count_updated, all_marked_read
  • MessagesEditMessageAsync, DeleteMessageAsync, PinMessageAsync, UnpinMessageAsync, GetPinnedMessagesAsyncmessage_edited, message_deleted, message_pinned, message_unpinned
  • Presence & statusSetStatusAsync, SetCustomStatusAsync, ClearCustomStatusAsync, SetDNDAsync, ClearDNDAsync, GetUserPresenceAsyncuser_status_changed, custom_status_updated, dnd_status_changed
  • ChannelsCreateChannelAsync, UpdateChannelAsync, ArchiveChannelAsync, InviteToChannelAsync, JoinChannelAsync, LeaveChannelAsync, GetChannelMembersAsyncchannel_created, channel_updated, user_invited, user_joined_channel, user_left_channel
  • DMsCreateDMAsync, SendDMAsync, GetDMConversationsAsyncdm_created, dm_received
  • NotificationsSubscribeNotificationsAsync, GetNotificationsAsync, MarkNotificationReadAsync, ClearNotificationsAsyncnotification, notification_read, notifications_cleared
  • SearchSearchMessagesAsync, 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 broadcastschallenge_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, 01

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:

<50ms
Latency
99.9%
Uptime
32KB
Max Message
1M+
Messages/sec

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