mcpField
C
csharp-client

csharp-client

github.com/clockworklabs开发25.4k52 次安装今天更新
代码仓库

关于 csharp-client

面向客户端开发者的 SpacetimeDB C# SDK 参考,介绍连接、数据订阅、Reducer 和缓存读取,并涵盖断线重连与令牌刷新。

主要功能

  1. 01连接 SpacetimeDB 数据库
  2. 02订阅表并查询行数据
  3. 03读取缓存并处理行事件
  4. 04调用 Reducer 并检查结果
  5. 05配置重连与令牌刷新
  6. 06使用 FrameTick() 处理网络事件

适用场景

  1. 01构建由 SpacetimeDB 支持的 C# 应用
  2. 02通过订阅持续更新客户端状态
  3. 03处理临时连接中断
  4. 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}");

评论

支持 Markdown