A C# client for Phoenix Channels.
Unity compatible. Powering Dama King.
- Full Phoenix Channels protocol support
- Modern async/await API with cancellation and typed lifecycle failures
- Automatic reconnection and channel rejoin
- Thread-safe socket, channel, push, scheduler, and presence state
- Presence tracking with async wait helpers
- Refreshable connection parameters for token rotation
- Structured operational and contained-error reporting
- Level-gated logging with built-in console and Unity sinks
- Customizable WebSocket and JSON implementations
- Unity and .NET Standard 2.0 compatible
dotnet add package PhoenixSharpInstall via openupm-cli:
openupm add io.level3.phoenixsharpOr add manually to Packages/manifest.json:
{
"scopedRegistries": [
{
"name": "package.openupm.com",
"url": "https://package.openupm.com",
"scopes": ["io.level3"]
}
],
"dependencies": {
"io.level3.phoenixsharp": "2.0.0"
}
}Alternatively, download the source and add it to your project, or use a Git submodule.
var options = new Socket.Options(new JsonMessageSerializer())
{
Logger = new ConsoleLogger()
};
// PhoenixSharp does not ship a transport. Use one of the adapters below.
var socket = new Socket(
"wss://example.com/socket",
null,
new MyWebSocketFactory(),
options
);
socket.OnError += error =>
Console.Error.WriteLine($"{error.Kind}: {error.Message}");
socket.OnUnhandledError += error =>
Console.Error.WriteLine($"Contained {error.Kind}: {error.Message}");
socket.Connect();
var channel = socket.Channel(
"room:lobby",
new Dictionary<string, object> { ["userId"] = "123" }
);
channel.On<ChatMessage>("new_message", payload =>
{
Console.WriteLine($"Received: {payload.Text}");
});
channel.Join();
channel.Push("send_message", new { text = "Hello!" });The same setup can use async lifecycle methods instead:
try
{
using var connectTimeout =
new CancellationTokenSource(TimeSpan.FromSeconds(15));
await socket.ConnectAsync(connectTimeout.Token);
var join = await channel.JoinAsync();
if (join.IsSuccess)
{
var push = await channel.PushAsync<ChatMessage>(
"send_message",
new { text = "Hello!" }
);
if (push.IsSuccess && push.Response != null)
{
Console.WriteLine($"Sent with id: {push.Response.Id}");
}
}
await channel.LeaveAsync();
await socket.DisconnectAsync();
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("Connection cancelled");
}
catch (PhoenixConnectionException exception)
{
Console.Error.WriteLine($"Connection failed: {exception.Message}");
}
catch (PhoenixException exception)
{
Console.Error.WriteLine($"Phoenix operation failed: {exception.Message}");
}Join and push error/timeout replies are represented by JoinResult and PushResult. Connection and disconnection failures fault with PhoenixConnectionException and PhoenixException; disposed use faults with ObjectDisposedException.
- Migration Guide - Upgrading from older versions
- Integration Tests - Complete usage examples
Use ParamsProvider for credentials that may change between connection attempts. It is called immediately before each transport is built and may be called concurrently, so the provider must be thread-safe. Do not also pass a static parameter dictionary to the Socket constructor.
var options = new Socket.Options(new JsonMessageSerializer())
{
ParamsProvider = () => new Dictionary<string, string>
{
["token"] = tokenProvider.GetAccessToken()
}
};
var socket = new Socket(address, null, factory, options);The library requires an IWebsocket implementation. Sample implementations are available in PhoenixTests/WebSocketImpl.
var factory = new MyWebSocketFactory();
var socket = new Socket(address, null, factory, options);Factories receive an immutable WebsocketConfiguration with Uri and callback properties. Build must not invoke callbacks until Connect() is called on the returned transport. State must be safe to read from any thread, and adapters should preserve close codes and reasons when their underlying transport supports them.
Recommended implementations:
- NativeWebSocket (Unity) - Open source, supports WebGL/Android/iOS/UWP. Install via UPM, then import the sample adapter from the package.
- BestHTTP (Unity) - Commercial plugin. See the sample adapter in the package.
- System.Net.WebSockets - Built-in .NET option, no additional dependencies
The default JsonMessageSerializer uses Newtonsoft.Json with the Phoenix V2 serialization format. To use a different serializer, implement IMessageSerializer and IJsonBox.
var options = new Socket.Options(new MyCustomSerializer());Socket.OnError receives operational PhoenixError values, including a channel's delivery-blocking OnMessage failure. Socket.OnUnhandledError receives failures PhoenixSharp deliberately contains so runtime processing can continue, such as an individual throwing event subscriber or a dropped inbound message. When no logger or OnUnhandledError handler is configured, the first unobserved contained error emits a fail-safe warning to Console.Error.
Use ConsoleLogger on .NET, or UnityLogger from the auto-referenced Phoenix.UnityLogger Unity assembly:
var options = new Socket.Options(new JsonMessageSerializer())
{
// Use new UnityLogger(LogLevel.Info) in Unity.
Logger = new ConsoleLogger(LogLevel.Info)
};Custom sinks own their filtering and must not throw:
public sealed class ErrorOnlyLogger : ILogger
{
public bool IsEnabled(LogLevel level, string source)
{
return level >= LogLevel.Error;
}
public void Log(
LogLevel level,
string source,
string message,
Exception? exception
)
{
try
{
Console.Error.WriteLine($"[{level}] [{source}] {message}");
if (exception != null)
{
Console.Error.WriteLine(exception);
}
}
catch
{
// ILogger implementations must not throw.
}
}
}var presence = new Presence(channel);
presence.OnJoin += (key, current, newPresence) =>
{
Console.WriteLine($"{key} joined");
};
presence.OnLeave += (key, current, leftPresence) =>
{
Console.WriteLine($"{key} left");
};
// Wait for initial state (async)
await presence.WaitForInitialSyncAsync();PhoenixSharp protects its internal state, but it does not serialize user code onto a particular thread. Treat every callback as thread-agnostic: socket delegates (OnOpen, OnClose, OnError, OnUnhandledError, and OnMessage), channel subscriptions, push reply hooks, presence events (OnJoin, OnLeave, and OnSync), and ILogger sinks may all run on any thread.
The default TaskDelayedExecutor runs timer callbacks on the thread pool and does not capture the current SynchronizationContext. Do not depend on a UI-context unhandled-exception hook: an exception that escapes a user callback running on the pool bypasses that UI hook. Wrap callback bodies and route failures explicitly; PhoenixSharp reports callback failures it contains through OnError or OnUnhandledError.
Unity consumers must marshal to the main thread before touching UnityEngine objects. The UniTask executor sample keeps PhoenixSharp's delayed callbacks on Unity's PlayerLoop, but it does not change the threading contract for transport and subscription callbacks. Queue work to a PlayerLoop or MonoBehaviour dispatcher, or use UniTask.SwitchToMainThread() in an async flow.
Socket and Presence callbacks (OnError, OnUnhandledError, OnJoin, OnLeave, OnSync, and friends) are C# events with thread-safe +=/-=, so handlers can be added or removed from any thread at any time. Handlers themselves may still fire on any thread, and a handler removed concurrently with a dispatch may run one final time.
For background on Unity's .NET support, see Microsoft's Unity scripting documentation.
For Unity, you can replace the default thread-pool executor via IDelayedExecutor. The recommended option is UniTask — import the sample from the package and use UniTaskDelayedExecutor for zero-allocation delays on Unity's PlayerLoop:
var options = new Socket.Options(serializer)
{
DelayedExecutor = new UniTaskDelayedExecutor()
};A coroutine-based executor is also available as a sample for projects that don't use UniTask.
The recommended WebSocket library for Unity is NativeWebSocket — an open-source, dependency-free WebSocket client that supports WebGL, Android, iOS, and UWP. Install it via UPM with the git URL, then import the NativeWebSocket sample adapter from the PhoenixSharp package in Unity's Package Manager.
Alternatively, BestHTTP is a commercial plugin. A sample adapter is also included in the package.
- UniTask - Zero-allocation async/await; use with the UniTask Delayed Executor sample for best performance
- NativeWebSocket - Open-source WebSocket client for Unity (WebGL/Android/iOS/UWP)
- BestHTTP - Commercial plugin with a package sample adapter
- com.unity.nuget.newtonsoft-json - Unity's official Newtonsoft.Json package
| Method | Description |
|---|---|
Connect() |
Connect to the server |
ConnectAsync() |
Connect asynchronously |
Disconnect() |
Disconnect from the server |
DisconnectAsync() |
Disconnect asynchronously |
Channel(topic, params) |
Create a channel |
| Method | Description |
|---|---|
Join() |
Join the channel |
JoinAsync() |
Join asynchronously, returns JoinResult |
Leave() |
Leave the channel |
LeaveAsync() |
Leave asynchronously |
Push(event, payload) |
Send a message |
PushAsync(event, payload) |
Send asynchronously, returns PushResult |
PushAsync<T>(event, payload) |
Send with typed response |
On(event, callback) |
Subscribe to events |
Off(event) |
Unsubscribe from events |
WaitForEventAsync(event) |
Wait for a single event |
| Method | Description |
|---|---|
OnJoin |
Event fired when a user joins |
OnLeave |
Event fired when a user leaves |
OnSync |
Event fired on state sync |
WaitForInitialSyncAsync() |
Wait for initial presence state |
WaitForUserAsync(key, timeout) |
Wait for a specific user |
# Offline suite, including allocation guards
dotnet test --filter "Category!=Integration"
# Exclude performance tests when iterating
dotnet test --filter "Category!=Integration&Category!=Performance"
# Integration tests (requires server)
dotnet test --filter "Category=Integration"Integration tests run against phoenix-sharp.level3.io. Server source: phoenix-integration-tester
Issues and pull requests are welcome!
MIT
Maz (Mazyad Alabduljaleel)
Logo is a mix of Unity and Phoenix logos. Please don't sue me.
