skills/csharp-client/SKILL.md
Install: dotnet add package SpacetimeDB.ClientSDK
using SpacetimeDB;
using SpacetimeDB.Types;
var conn = DbConnection.Builder()
.WithUri("http://localhost:3000")
.WithDatabaseName("my-database")
.WithToken(savedToken)
.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();
FrameTick() must be called in your main loop. The SDK queues all network messages and only processes them when you call FrameTick(). Without it, no callbacks fire.
while (running)
{
conn.FrameTick();
// Your application logic...
Thread.Sleep(16); // ~60fps
}
Thread safety: FrameTick() processes messages on the calling thread. Do NOT call it from a background thread. Do NOT access conn.Db from background threads.
// 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!");
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}");
};
// Identities from OnConnect callback
Identity myIdentity;
// Compare identities
if (player.Owner == myIdentity) { /* it's me */ }
// Display
Console.WriteLine($"Identity: {identity}");