csharp-client について
SpacetimeDB アプリケーションに接続するための C# クライアント SDK リファレンスです。データ購読、Reducer、キャッシュ参照に加え、再接続とトークン更新を扱います。
主な機能
- 01SpacetimeDB データベースへの接続
- 02テーブル購読と行データのクエリ
- 03キャッシュ参照と行イベント処理
- 04Reducer 呼び出しと結果確認
- 05再接続とトークン更新の設定
- 06FrameTick() によるネットワークイベント処理
活用シーン
- 01SpacetimeDB を利用する C# アプリの構築
- 02購読によるクライアント状態の更新
- 03一時的な接続断への対応
- 04再接続前の認証情報更新
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ clockworklabs/ spacetimedb --skill csharp-clientインストール後、Claude Code で/csharp-clientを入力して読み込まれたか確認します。
詳細
SpacetimeDB C# クライアント
インストール:dotnet add package SpacetimeDB.ClientSDK
生成されたバインディングでは、行フィールドを含め、snake_case の名前が PascalCase に変換されます。サーバーの列 trip_id は、クライアントの行では TripId になります。
接続
using SpacetimeDB;
using SpacetimeDB.Types;
var conn = DbConnection.Builder()
.WithUri("http://localhost:3000")
.WithDatabaseName("my-database")
.WithToken(savedToken)
.WithCompression(Compression.Brotli) // optional; this is the default
.OnConnect((conn, identity, token) =>
{
Console.WriteLine($"Connected as: {identity}");
// Save token for reconnection
File.WriteAllText("auth_token.txt", token);
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.SubscribeToAllTables();
})
.OnConnectError(err => Console.Error.WriteLine($"Connection failed: {err}"))
.OnDisconnect((conn, err) =>
{
if (err != null) Console.Error.WriteLine($"Disconnected: {err}");
})
.Build();
圧縮オプションは Compression.Brotli、Compression.Gzip、Compression.None です。WithCompression を省略すると、SDK は Brotli を使用します。
自動再接続とトークンの更新
自動再接続はオプトインです。有効にするには、ビルダーに .WithAutomaticReconnect() を追加します。初回の接続失敗時は再試行されません。確立済みの接続が失われると、SDK は MinDelay(デフォルトは 1 s)から MaxDelay(デフォルトは 30 s)まで、指数バックオフとジッターを使用して再試行します。.WithAutomaticReconnect(new AutomaticReconnectOptions { MinDelay = ..., MaxDelay = ... }) で調整できます。500 ms と 1 s の下限を下回る値は警告とともに引き上げられます。これにより、再試行中のクライアントがデータベースに過剰な負荷をかけるのを防ぎます。Disconnect() を呼び出すと、再接続は完全に停止します。
IsActive が false の間も FrameTick() の呼び出しを続けてください。再接続中で、次のハンドシェイク前の場合、IsReconnecting は true になります。NextReconnect? を確認するには、.OnDisconnect((conn, error, next) => ...) と .OnConnectError((error, next) => ...) を使用します。null 以外の値には Attempt と Delay が含まれ、null の場合は再試行が予定されていません。既存のコールバックのオーバーロードも引き続き使用できます。
OnConnect は再接続が成功するたびに実行されるため、上記の例でサブスクリプションを作成するのは初回接続時のみにするか、Build() の後に移動してください。行コールバックも一度だけ登録してください。同じ接続、ID、テーブルハンドル、サブスクリプションが維持されます。各試行では新しい ConnectionId が割り当てられます。キャッシュされた行は読み取り可能ですが、障害中は最新ではありません。SDK はサブスクリプションを 1 つのバッチで再実行し、差分として生じた行変更のみを通知します。サブスクリプションの OnApplied は再実行後にも呼び出されます。繰り返し行わない初回設定ではなく、データの準備完了を示す用途に使ってください。
有効期限のある認証情報では、.WithToken(initialToken) と .WithTokenProvider(() => RefreshTokenAsync()) を組み合わせます。プロバイダーは同じ ID に対する Task<string> を返す必要があります。プロバイダーは初回接続時には呼び出されません。再試行の前に、残りの有効期間が 30 秒以下、またはトークンの有効期間の 5%以下のいずれか大きい方の場合、有効期限を読み取れない場合、または再利用したトークンが拒否された場合に呼び出されます。プロバイダーの失敗は再試行されますが、新たに提供されたトークンが拒否された場合は再試行されません。接続中に定期的な更新は行われません。切断すると、保留中のプロバイダー結果は無視されますが、プロバイダー自身の処理はキャンセルされません。
切断中に行った呼び出しは、即座に失敗します。保留中のリデューサー呼び出しには Status.UnknownResult が返され、保留中のプロシージャと単発クエリは UnknownResultException で失敗します。これらの呼び出しはすでに実行されている可能性があり、再実行されることはありません。未処理の未知のリデューサー結果が OnUnhandledReducerError に届くように、バインディングを再生成してください。
イベントループ(重要)
FrameTick() はメインループで呼び出す必要があります。 SDK はすべてのネットワークメッセージをキューに格納し、FrameTick() の呼び出し時にのみ処理します。これを呼び出さないと、コールバックは実行されません。
while (running)
{
conn.FrameTick();
// Your application logic...
Thread.Sleep(16); // ~60fps
}
スレッドセーフティ:FrameTick() は呼び出し元のスレッドでメッセージを処理します。バックグラウンドスレッドから呼び出してはいけません。バックグラウンドスレッドから conn.Db にアクセスしてはいけません。
サブスクリプション
// Subscribe to all tables
conn.SubscriptionBuilder()
.OnApplied(ctx => Console.WriteLine("Subscription ready"))
.SubscribeToAllTables();
// Subscribe with typed query builder (recommended)
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.AddQuery(q => q.From.Player().Where(p => p.Level.Gte(5u)))
.AddQuery(q => q.From.GameState())
.Subscribe();
// Or with raw SQL strings
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.Subscribe(new[] {
"SELECT * FROM player WHERE level >= 5",
"SELECT * FROM game_state"
});
行コールバック
conn.Db.Player.OnInsert += (EventContext ctx, Player player) =>
{
Console.WriteLine($"Player joined: {player.Name}");
};
conn.Db.Player.OnDelete += (EventContext ctx, Player player) =>
{
Console.WriteLine($"Player left: {player.Name}");
};
conn.Db.Player.OnUpdate += (EventContext ctx, Player oldPlayer, Player newPlayer) =>
{
Console.WriteLine($"Player updated: {newPlayer.Name}");
};
クライアントキャッシュの読み取り
// Find by primary key
if (conn.Db.Player.Id.Find(playerId) is Player player)
{
Console.WriteLine($"Player: {player.Name}");
}
// Find by unique column
var me = conn.Db.Player.Identity.Find(myIdentity);
// Filter by indexed column
foreach (var p in conn.Db.Player.Level.Filter(5))
{
Console.WriteLine($"Level 5: {p.Name}");
}
// Iterate all rows
foreach (var p in conn.Db.Player.Iter())
{
Console.WriteLine(p.Name);
}
// Count
int total = conn.Db.Player.Count;
リデューサーの呼び出し
conn.Reducers.CreatePlayer("Alice");
conn.Reducers.MovePlayer(10.0f, 20.0f);
conn.Reducers.SendMessage("Hello!");
リデューサーの呼び出しは void を返します。失敗は、以下のリデューサーコールバック(Status.Failed)で確認してください。
リデューサーコールバック
conn.Reducers.OnSendMessage += (ReducerEventContext ctx, string text) =>
{
if (ctx.Event.Status is Status.Committed)
Console.WriteLine($"Message sent: {text}");
else if (ctx.Event.Status is Status.Failed(var reason))
Console.Error.WriteLine($"Send failed: {reason}");
else if (ctx.Event.Status is Status.UnknownResult)
Console.Error.WriteLine("Connection lost before the result arrived; the message may have been sent.");
};
ID
// Identities from OnConnect callback
Identity myIdentity;
// Compare identities
if (player.Owner == myIdentity) { /* it's me */ }
// Display
Console.WriteLine($"Identity: {identity}");