关于 csharp-client
面向客户端开发者的 SpacetimeDB C# SDK 参考,介绍连接、数据订阅、Reducer 和缓存读取,并涵盖断线重连与令牌刷新。
主要功能
- 01连接 SpacetimeDB 数据库
- 02订阅表并查询行数据
- 03读取缓存并处理行事件
- 04调用 Reducer 并检查结果
- 05配置重连与令牌刷新
- 06使用 FrameTick() 处理网络事件
适用场景
- 01构建由 SpacetimeDB 支持的 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。使用 .OnDisconnect((conn, error, next) => ...) 和 .OnConnectError((error, next) => ...) 检查 NextReconnect?:非 null 值会提供 Attempt 和 Delay;null 表示不会安排重试。现有的回调重载仍可使用。
每次成功重连时都会运行 OnConnect,因此只在首次连接时创建上文示例中的订阅,或将其移到 Build() 之后。行回调也只需注册一次。同一连接、身份、表句柄和订阅会一直保留;每次尝试都会获取新的 ConnectionId。断连期间,缓存行仍可读取,但数据已过时。SDK 会批量重放订阅,并且只发出最终的行变更。重放后会再次运行订阅的 OnApplied;请用它标记数据已就绪,而不要用于重复的一次性设置。
对于会过期的凭据,请将 .WithToken(initialToken) 与 .WithTokenProvider(() => RefreshTokenAsync()) 结合使用;提供程序应为同一身份返回 Task<string>。初始连接不会调用该提供程序。重试前,如果令牌剩余有效期不超过 30 秒或令牌有效期的 5%(取两者中较大者)、无法读取过期时间,或重用的令牌被拒绝,都会调用该提供程序。提供程序失败会触发重试;新提供的令牌被拒绝则会终止连接。连接期间不会定期刷新。断开连接会忽略尚未返回的提供程序结果,但不会取消提供程序自身的工作。
断开连接时发出的调用会立即失败。待处理的 reducer 调用会收到 Status.UnknownResult;待处理的过程调用和一次性查询会失败并抛出 UnknownResultException。这些调用可能已经执行,且绝不会重放。请重新生成绑定,以便未处理的未知 reducer 结果能够传递到 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;
调用 Reducer
conn.Reducers.CreatePlayer("Alice");
conn.Reducers.MovePlayer(10.0f, 20.0f);
conn.Reducers.SendMessage("Hello!");
Reducer 调用会返回 void;可通过下方的 reducer 回调(Status.Failed)查看失败情况。
Reducer 回调
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.");
};
身份
// Identities from OnConnect callback
Identity myIdentity;
// Compare identities
if (player.Owner == myIdentity) { /* it's me */ }
// Display
Console.WriteLine($"Identity: {identity}");