spacetimedb-csharp
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSpacetimeDB C# SDK
SpacetimeDB C# SDK
This skill provides guidance for building C# server-side modules and C# clients that connect to SpacetimeDB 2.0.
本指南提供了构建连接到SpacetimeDB 2.0的C#服务器端模块与C#客户端的相关指导。
HALLUCINATED APIs — DO NOT USE
虚构API — 请勿使用
These APIs DO NOT EXIST. LLMs frequently hallucinate them.
csharp
// WRONG — these table access patterns do not exist
ctx.db.tableName // Wrong casing — use ctx.Db
ctx.Db.tableName // Wrong casing — accessor must match exactly
ctx.Db.TableName.Get(id) // Use Find, not Get
ctx.Db.TableName.FindById(id) // Use index accessor: ctx.Db.TableName.Id.Find(id)
ctx.Db.table.field_name.Find(x) // Wrong! Use PascalCase: ctx.Db.Table.FieldName.Find(x)
Optional<string> field; // Use C# nullable: string? field
// WRONG — missing partial keyword
public struct MyTable { } // Must be "partial struct"
public class Module { } // Must be "static partial class"
// WRONG — non-partial types
[SpacetimeDB.Table(Accessor = "Player")]
public struct Player { } // WRONG — missing partial!
// WRONG — sum type syntax (VERY COMMON MISTAKE)
public partial struct Shape : TaggedEnum<(Circle, Rectangle)> { } // WRONG: struct, missing names
public partial record Shape : TaggedEnum<(Circle, Rectangle)> { } // WRONG: missing variant names
public partial class Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { } // WRONG: class
// WRONG — Index attribute without full qualification
[Index.BTree(Accessor = "idx", Columns = new[] { "Col" })] // Ambiguous with System.Index!
[SpacetimeDB.Index.BTree(Accessor = "idx", Columns = ["Col"])] // Valid with modern C# collection expressions
// WRONG — old 1.0 patterns
[SpacetimeDB.Table(Name = "Player")] // Use Accessor, not Name (2.0)
<PackageReference Include="SpacetimeDB.ServerSdk" /> // Use SpacetimeDB.Runtime
.WithModuleName("my-db") // Use .WithDatabaseName() (2.0)
ScheduleAt.Time(futureTime) // Use new ScheduleAt.Time(futureTime)
// WRONG — lifecycle hooks starting with "On"
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
public static void OnClientConnected(ReducerContext ctx) { } // STDB0010 error!
// WRONG — non-deterministic code in reducers
var random = new Random(); // Use ctx.Rng
var guid = Guid.NewGuid(); // Not allowed
var now = DateTime.Now; // Use ctx.Timestamp
// WRONG — collection parameters
int[] itemIds = { 1, 2, 3 };
_conn.Reducers.ProcessItems(itemIds); // Generated code expects List<T>!这些API并不存在。大语言模型经常会虚构出此类API。
csharp
// 错误示例 — 这些表访问方式不存在
ctx.db.tableName // 大小写错误 — 请使用 ctx.Db
ctx.Db.tableName // 大小写错误 — 访问器必须完全匹配
ctx.Db.TableName.Get(id) // 使用 Find,而非 Get
ctx.Db.TableName.FindById(id) // 使用索引访问器:ctx.Db.TableName.Id.Find(id)
ctx.Db.table.field_name.Find(x) // 错误!请使用 PascalCase:ctx.Db.Table.FieldName.Find(x)
Optional<string> field; // 使用C#可空类型:string? field
// 错误示例 — 缺少 partial 关键字
public struct MyTable { } // 必须为 "partial struct"
public class Module { } // 必须为 "static partial class"
// 错误示例 — 非 partial 类型
[SpacetimeDB.Table(Accessor = "Player")]
public struct Player { } // 错误 — 缺少 partial!
// 错误示例 — 求和类型语法(非常常见的错误)
public partial struct Shape : TaggedEnum<(Circle, Rectangle)> { } // 错误:结构体,缺少名称
public partial record Shape : TaggedEnum<(Circle, Rectangle)> { } // 错误:缺少变体名称
public partial class Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { } // 错误:类
// 错误示例 — 未完全限定的 Index 属性
[Index.BTree(Accessor = "idx", Columns = new[] { "Col" })] // 与 System.Index 存在歧义!
[SpacetimeDB.Index.BTree(Accessor = "idx", Columns = ["Col"])] // 使用现代C#集合表达式才有效
// 错误示例 — 旧版1.0模式
[SpacetimeDB.Table(Name = "Player")] // 使用 Accessor,而非 Name(2.0版本)
<PackageReference Include="SpacetimeDB.ServerSdk" /> // 使用 SpacetimeDB.Runtime
.WithModuleName("my-db") // 使用 .WithDatabaseName()(2.0版本)
ScheduleAt.Time(futureTime) // 使用 new ScheduleAt.Time(futureTime)
// 错误示例 — 以"On"开头的生命周期钩子
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
public static void OnClientConnected(ReducerContext ctx) { } // 触发STDB0010错误!
// 错误示例 — 归约器中的非确定性代码
var random = new Random(); // 使用 ctx.Rng
var guid = Guid.NewGuid(); // 不允许使用
var now = DateTime.Now; // 使用 ctx.Timestamp
// 错误示例 — 集合参数
int[] itemIds = { 1, 2, 3 };
_conn.Reducers.ProcessItems(itemIds); // 生成的代码期望传入 List<T>!CORRECT PATTERNS
正确用法示例
csharp
using SpacetimeDB;
// CORRECT TABLE — must be partial struct, use Accessor
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
public partial struct Player
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
[SpacetimeDB.Index.BTree]
public Identity OwnerId;
public string Name;
}
// CORRECT MODULE — must be static partial class
public static partial class Module
{
[SpacetimeDB.Reducer]
public static void CreatePlayer(ReducerContext ctx, string name)
{
ctx.Db.Player.Insert(new Player { Id = 0, OwnerId = ctx.Sender, Name = name });
}
}
// CORRECT DATABASE ACCESS — PascalCase, index-based lookups
var player = ctx.Db.Player.Id.Find(playerId); // Unique/PK: returns nullable
foreach (var p in ctx.Db.Player.OwnerId.Filter(ctx.Sender)) { } // BTree: returns IEnumerable
// CORRECT SUM TYPE — partial record with named tuple elements
[SpacetimeDB.Type]
public partial record Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }
// CORRECT — collection parameters use List<T>
_conn.Reducers.ProcessItems(new List<int> { 1, 2, 3 });csharp
using SpacetimeDB;
// 正确的表定义 — 必须为 partial struct,使用 Accessor
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
public partial struct Player
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
[SpacetimeDB.Index.BTree]
public Identity OwnerId;
public string Name;
}
// 正确的模块定义 — 必须为 static partial class
public static partial class Module
{
[SpacetimeDB.Reducer]
public static void CreatePlayer(ReducerContext ctx, string name)
{
ctx.Db.Player.Insert(new Player { Id = 0, OwnerId = ctx.Sender, Name = name });
}
}
// 正确的数据库访问 — 使用 PascalCase,基于索引查找
var player = ctx.Db.Player.Id.Find(playerId); // 唯一/主键:返回可空类型
foreach (var p in ctx.Db.Player.OwnerId.Filter(ctx.Sender)) { } // B树索引:返回 IEnumerable
// 正确的求和类型 — 带命名元组元素的 partial record
[SpacetimeDB.Type]
public partial record Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }
// 正确示例 — 集合参数使用 List<T>
_conn.Reducers.ProcessItems(new List<int> { 1, 2, 3 });Common Mistakes Table
常见错误对照表
| Wrong | Right | Error |
|---|---|---|
| Wrong .csproj name | | Publish fails silently |
| .NET 9 SDK | .NET 8 SDK only | WASI compilation fails |
| Missing WASI workload | | Build fails |
| async/await in reducers | Synchronous only | Not supported |
| | Update only via primary key (2.0) |
Not calling | | No callbacks fire |
Accessing | Copy data in callback | Data races |
| 错误用法 | 正确用法 | 错误说明 |
|---|---|---|
| 错误的.csproj文件名 | | 发布过程会静默失败 |
| .NET 9 SDK | 仅使用.NET 8 SDK | WASI编译失败 |
| 缺少WASI工作负载 | | 构建失败 |
| 归约器中使用async/await | 仅支持同步代码 | 不支持异步 |
| | 2.0版本仅支持通过主键更新 |
未调用 | 在更新循环中调用 | 不会触发任何回调 |
从后台线程访问 | 在回调中复制数据 | 会导致数据竞争 |
Hard Requirements
硬性要求
- Tables and Module MUST be — required for code generation
partial - Use in table attributes —
Accessor =is only for SQL compatibility (2.0)Name = - Project file MUST be named — CLI requirement
StdbModule.csproj - Requires .NET 8 SDK — .NET 9 and newer not yet supported
- Install WASI workload —
dotnet workload install wasi-experimental - Procedures are supported — use with
[SpacetimeDB.Procedure]when neededProcedureContext - Reducers must be deterministic — no filesystem, network, timers, or
Random - Add — if clients need to subscribe to a table
Public = true - Use for nullable fields — not
T?Optional<T> - Pass for auto-increment — to trigger ID generation on insert
0 - Sum types must be — not struct or class
partial record - Fully qualify Index attribute — to avoid System.Index ambiguity
[SpacetimeDB.Index.BTree] - Update only via primary key — use delete+insert for non-PK changes (2.0)
- Use package — not
SpacetimeDB.Runtime(2.0)ServerSdk - Use for collection parameters — not arrays
List<T> - is in
Identitynamespace — notSpacetimeDBSpacetimeDB.Types
- 表和模块必须标记为— 代码生成的必要条件
partial - 在表属性中使用—
Accessor =仅用于SQL兼容性(2.0版本)Name = - 项目文件必须命名为— CLI工具的要求
StdbModule.csproj - 需要.NET 8 SDK — .NET 9及更高版本暂不支持
- 安装WASI工作负载 — 执行
dotnet workload install wasi-experimental - 支持过程调用 — 必要时使用和
[SpacetimeDB.Procedure]ProcedureContext - 归约器必须是确定性的 — 禁止访问文件系统、网络、计时器或使用
Random - 添加— 如果客户端需要订阅该表
Public = true - 使用表示可空字段 — 不要使用
T?Optional<T> - 自增字段传入— 插入时触发ID生成
0 - 求和类型必须是— 不能是结构体或类
partial record - 完全限定Index属性 — 使用避免与System.Index冲突
[SpacetimeDB.Index.BTree] - 仅通过主键更新 — 非主键变更请使用删除+插入(2.0版本)
- 使用包 — 不要使用
SpacetimeDB.Runtime(2.0版本)ServerSdk - 集合参数使用— 不要使用数组
List<T> - 位于
Identity命名空间 — 不是SpacetimeDBSpacetimeDB.Types
Server-Side Module Development
服务器端模块开发
Table Definition
表定义
csharp
using SpacetimeDB;
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
public partial struct Player
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
[SpacetimeDB.Index.BTree]
public Identity OwnerId;
public string Name;
public Timestamp CreatedAt;
}
// Multi-column index (use fully-qualified attribute!)
[SpacetimeDB.Table(Accessor = "Score", Public = true)]
[SpacetimeDB.Index.BTree(Accessor = "by_player_game", Columns = new[] { "PlayerId", "GameId" })]
public partial struct Score
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
public Identity PlayerId;
public string GameId;
public int Points;
}csharp
using SpacetimeDB;
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
public partial struct Player
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
[SpacetimeDB.Index.BTree]
public Identity OwnerId;
public string Name;
public Timestamp CreatedAt;
}
// 多列索引(必须使用完全限定的属性!)
[SpacetimeDB.Table(Accessor = "Score", Public = true)]
[SpacetimeDB.Index.BTree(Accessor = "by_player_game", Columns = new[] { "PlayerId", "GameId" })]
public partial struct Score
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
public Identity PlayerId;
public string GameId;
public int Points;
}Field Attributes
字段属性
csharp
[SpacetimeDB.PrimaryKey] // Exactly one per table (required)
[SpacetimeDB.AutoInc] // Auto-increment (integer fields only)
[SpacetimeDB.Unique] // Unique constraint
[SpacetimeDB.Index.BTree] // Single-column B-tree index
[SpacetimeDB.Default(value)] // Default value for new columnscsharp
[SpacetimeDB.PrimaryKey] // 每个表必须且只能有一个主键
[SpacetimeDB.AutoInc] // 自增(仅适用于整数字段)
[SpacetimeDB.Unique] // 唯一约束
[SpacetimeDB.Index.BTree] // 单列B树索引
[SpacetimeDB.Default(value)] // 新字段的默认值SpacetimeDB Column Types
SpacetimeDB列类型
csharp
Identity // User identity (SpacetimeDB namespace, not SpacetimeDB.Types)
Timestamp // Timestamp (use ctx.Timestamp server-side, never DateTime.Now)
ScheduleAt // For scheduled tables
T? // Nullable (e.g., string?)
List<T> // Collections (use List, not arrays)Standard C# primitives (, .., , , ) are all supported.
boolbyteulongfloatdoublestringcsharp
Identity // 用户身份(位于SpacetimeDB命名空间,而非SpacetimeDB.Types)
Timestamp // 时间戳(服务器端使用ctx.Timestamp,禁止使用DateTime.Now)
ScheduleAt // 用于定时表
T? // 可空类型(例如string?)
List<T> // 集合(使用List,而非数组)标准C#基元类型(、..、、、)均受支持。
boolbyteulongfloatdoublestringInsert with Auto-Increment
自增插入
csharp
var player = ctx.Db.Player.Insert(new Player
{
Id = 0, // Pass 0 to trigger auto-increment
OwnerId = ctx.Sender,
Name = name,
CreatedAt = ctx.Timestamp
});
ulong newId = player.Id; // Insert returns the row with generated IDcsharp
var player = ctx.Db.Player.Insert(new Player
{
Id = 0, // 传入0触发自增
OwnerId = ctx.Sender,
Name = name,
CreatedAt = ctx.Timestamp
});
ulong newId = player.Id; // Insert方法返回包含生成ID的行Module and Reducers
模块与归约器
csharp
using SpacetimeDB;
public static partial class Module
{
[SpacetimeDB.Reducer]
public static void CreateTask(ReducerContext ctx, string title)
{
if (string.IsNullOrEmpty(title))
throw new Exception("Title cannot be empty");
ctx.Db.Task.Insert(new Task
{
Id = 0,
OwnerId = ctx.Sender,
Title = title,
Completed = false
});
}
[SpacetimeDB.Reducer]
public static void CompleteTask(ReducerContext ctx, ulong taskId)
{
if (ctx.Db.Task.Id.Find(taskId) is not Task task)
throw new Exception("Task not found");
if (task.OwnerId != ctx.Sender)
throw new Exception("Not authorized");
ctx.Db.Task.Id.Update(task with { Completed = true });
}
[SpacetimeDB.Reducer]
public static void DeleteTask(ReducerContext ctx, ulong taskId)
{
ctx.Db.Task.Id.Delete(taskId);
}
}csharp
using SpacetimeDB;
public static partial class Module
{
[SpacetimeDB.Reducer]
public static void CreateTask(ReducerContext ctx, string title)
{
if (string.IsNullOrEmpty(title))
throw new Exception("标题不能为空");
ctx.Db.Task.Insert(new Task
{
Id = 0,
OwnerId = ctx.Sender,
Title = title,
Completed = false
});
}
[SpacetimeDB.Reducer]
public static void CompleteTask(ReducerContext ctx, ulong taskId)
{
if (ctx.Db.Task.Id.Find(taskId) is not Task task)
throw new Exception("任务不存在");
if (task.OwnerId != ctx.Sender)
throw new Exception("无权限操作");
ctx.Db.Task.Id.Update(task with { Completed = true });
}
[SpacetimeDB.Reducer]
public static void DeleteTask(ReducerContext ctx, ulong taskId)
{
ctx.Db.Task.Id.Delete(taskId);
}
}Lifecycle Reducers
生命周期归约器
csharp
public static partial class Module
{
[SpacetimeDB.Reducer(ReducerKind.Init)]
public static void Init(ReducerContext ctx)
{
Log.Info("Module initialized");
}
// CRITICAL: no "On" prefix!
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
public static void ClientConnected(ReducerContext ctx)
{
Log.Info($"Client connected: {ctx.Sender}");
if (ctx.Db.User.Identity.Find(ctx.Sender) is User user)
{
ctx.Db.User.Identity.Update(user with { Online = true });
}
else
{
ctx.Db.User.Insert(new User { Identity = ctx.Sender, Online = true });
}
}
[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)]
public static void ClientDisconnected(ReducerContext ctx)
{
if (ctx.Db.User.Identity.Find(ctx.Sender) is User user)
{
ctx.Db.User.Identity.Update(user with { Online = false });
}
}
}csharp
public static partial class Module
{
[SpacetimeDB.Reducer(ReducerKind.Init)]
public static void Init(ReducerContext ctx)
{
Log.Info("模块初始化完成");
}
// 重要:不要添加"On"前缀!
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
public static void ClientConnected(ReducerContext ctx)
{
Log.Info($"客户端已连接: {ctx.Sender}");
if (ctx.Db.User.Identity.Find(ctx.Sender) is User user)
{
ctx.Db.User.Identity.Update(user with { Online = true });
}
else
{
ctx.Db.User.Insert(new User { Identity = ctx.Sender, Online = true });
}
}
[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)]
public static void ClientDisconnected(ReducerContext ctx)
{
if (ctx.Db.User.Identity.Find(ctx.Sender) is User user)
{
ctx.Db.User.Identity.Update(user with { Online = false });
}
}
}Event Tables (2.0)
事件表(2.0版本)
Reducer callbacks are removed in 2.0. Use event tables + instead.
OnInsertcsharp
[SpacetimeDB.Table(Accessor = "DamageEvent", Public = true, Event = true)]
public partial struct DamageEvent
{
public Identity Target;
public uint Amount;
}
[SpacetimeDB.Reducer]
public static void DealDamage(ReducerContext ctx, Identity target, uint amount)
{
ctx.Db.DamageEvent.Insert(new DamageEvent { Target = target, Amount = amount });
}Client subscribes and uses :
OnInsertcsharp
conn.Db.DamageEvent.OnInsert += (ctx, evt) => {
PlayDamageAnimation(evt.Target, evt.Amount);
};Event tables must be subscribed explicitly — they are excluded from .
SubscribeToAllTables()2.0版本已移除归约器回调,请使用事件表 + 替代。
OnInsertcsharp
[SpacetimeDB.Table(Accessor = "DamageEvent", Public = true, Event = true)]
public partial struct DamageEvent
{
public Identity Target;
public uint Amount;
}
[SpacetimeDB.Reducer]
public static void DealDamage(ReducerContext ctx, Identity target, uint amount)
{
ctx.Db.DamageEvent.Insert(new DamageEvent { Target = target, Amount = amount });
}客户端订阅并使用:
OnInsertcsharp
conn.Db.DamageEvent.OnInsert += (ctx, evt) => {
PlayDamageAnimation(evt.Target, evt.Amount);
};事件表必须显式订阅 — 它们不会被包含在中。
SubscribeToAllTables()Database Access
数据库访问
csharp
// Find by primary key — returns nullable, use pattern matching
if (ctx.Db.Task.Id.Find(taskId) is Task task) { /* use task */ }
// Update by primary key (2.0: only primary key has .Update)
ctx.Db.Task.Id.Update(task with { Title = newTitle });
// Delete by primary key
ctx.Db.Task.Id.Delete(taskId);
// Find by unique index — returns nullable
if (ctx.Db.Player.Username.Find("alice") is Player player) { }
// Filter by B-tree index — returns iterator
foreach (var task in ctx.Db.Task.OwnerId.Filter(ctx.Sender)) { }
// Full table scan — avoid for large tables
foreach (var task in ctx.Db.Task.Iter()) { }
var count = ctx.Db.Task.Count;csharp
// 通过主键查找 — 返回可空类型,使用模式匹配
if (ctx.Db.Task.Id.Find(taskId) is Task task) { /* 使用task */ }
// 通过主键更新(2.0版本:仅主键支持.Update)
ctx.Db.Task.Id.Update(task with { Title = newTitle });
// 通过主键删除
ctx.Db.Task.Id.Delete(taskId);
// 通过唯一索引查找 — 返回可空类型
if (ctx.Db.Player.Username.Find("alice") is Player player) { }
// 通过B树索引过滤 — 返回迭代器
foreach (var task in ctx.Db.Task.OwnerId.Filter(ctx.Sender)) { }
// 全表扫描 — 大表请避免使用
foreach (var task in ctx.Db.Task.Iter()) { }
var count = ctx.Db.Task.Count;Custom Types and Sum Types
自定义类型与求和类型
csharp
[SpacetimeDB.Type]
public partial struct Position { public int X; public int Y; }
// Sum types MUST be partial record with named tuple
[SpacetimeDB.Type]
public partial struct Circle { public int Radius; }
[SpacetimeDB.Type]
public partial struct Rectangle { public int Width; public int Height; }
[SpacetimeDB.Type]
public partial record Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }
// Creating sum type values
var circle = new Shape.Circle(new Circle { Radius = 10 });csharp
[SpacetimeDB.Type]
public partial struct Position { public int X; public int Y; }
// 求和类型必须是带命名元组的partial record
[SpacetimeDB.Type]
public partial struct Circle { public int Radius; }
[SpacetimeDB.Type]
public partial struct Rectangle { public int Width; public int Height; }
[SpacetimeDB.Type]
public partial record Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }
// 创建求和类型值
var circle = new Shape.Circle(new Circle { Radius = 10 });Scheduled Tables
定时表
csharp
[SpacetimeDB.Table(Accessor = "Reminder", Scheduled = nameof(Module.SendReminder))]
public partial struct Reminder
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
public string Message;
public ScheduleAt ScheduledAt;
}
public static partial class Module
{
[SpacetimeDB.Reducer]
public static void SendReminder(ReducerContext ctx, Reminder reminder)
{
Log.Info($"Reminder: {reminder.Message}");
}
[SpacetimeDB.Reducer]
public static void CreateReminder(ReducerContext ctx, string message, ulong delaySecs)
{
ctx.Db.Reminder.Insert(new Reminder
{
Id = 0,
Message = message,
ScheduledAt = new ScheduleAt.Time(ctx.Timestamp + TimeSpan.FromSeconds(delaySecs))
});
}
}csharp
[SpacetimeDB.Table(Accessor = "Reminder", Scheduled = nameof(Module.SendReminder))]
public partial struct Reminder
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
public string Message;
public ScheduleAt ScheduledAt;
}
public static partial class Module
{
[SpacetimeDB.Reducer]
public static void SendReminder(ReducerContext ctx, Reminder reminder)
{
Log.Info($"提醒: {reminder.Message}");
}
[SpacetimeDB.Reducer]
public static void CreateReminder(ReducerContext ctx, string message, ulong delaySecs)
{
ctx.Db.Reminder.Insert(new Reminder
{
Id = 0,
Message = message,
ScheduledAt = new ScheduleAt.Time(ctx.Timestamp + TimeSpan.FromSeconds(delaySecs))
});
}
}Logging
日志
csharp
Log.Debug("Debug message");
Log.Info("Information");
Log.Warn("Warning");
Log.Error("Error occurred");
Log.Exception("Critical failure"); // Logs at error levelcsharp
Log.Debug("调试消息");
Log.Info("信息消息");
Log.Warn("警告消息");
Log.Error("发生错误");
Log.Exception("严重故障"); // 以错误级别记录ReducerContext API
ReducerContext API
csharp
ctx.Sender // Identity of the caller
ctx.Timestamp // Current timestamp
ctx.Db // Database access
ctx.Identity // Module's own identity
ctx.ConnectionId // Connection ID (nullable)
ctx.SenderAuth // Authorization context (JWT claims, internal call detection)
ctx.Rng // Deterministic random number generatorcsharp
ctx.Sender // 调用者的身份标识
ctx.Timestamp // 当前时间戳
ctx.Db // 数据库访问入口
ctx.Identity // 模块自身的身份标识
ctx.ConnectionId // 连接ID(可空)
ctx.SenderAuth // 授权上下文(JWT声明、内部调用检测)
ctx.Rng // 确定性随机数生成器Error Handling
错误处理
Throwing an exception in a reducer rolls back the entire transaction:
csharp
[SpacetimeDB.Reducer]
public static void TransferCredits(ReducerContext ctx, Identity toUser, uint amount)
{
if (ctx.Db.User.Identity.Find(ctx.Sender) is not User sender)
throw new Exception("Sender not found");
if (sender.Credits < amount)
throw new Exception("Insufficient credits");
ctx.Db.User.Identity.Update(sender with { Credits = sender.Credits - amount });
if (ctx.Db.User.Identity.Find(toUser) is User receiver)
ctx.Db.User.Identity.Update(receiver with { Credits = receiver.Credits + amount });
}在归约器中抛出异常会回滚整个事务:
csharp
[SpacetimeDB.Reducer]
public static void TransferCredits(ReducerContext ctx, Identity toUser, uint amount)
{
if (ctx.Db.User.Identity.Find(ctx.Sender) is not User sender)
throw new Exception("发送方不存在");
if (sender.Credits < amount)
throw new Exception("余额不足");
ctx.Db.User.Identity.Update(sender with { Credits = sender.Credits - amount });
if (ctx.Db.User.Identity.Find(toUser) is User receiver)
ctx.Db.User.Identity.Update(receiver with { Credits = receiver.Credits + amount });
}Project Setup
项目设置
Required .csproj (MUST be named StdbModule.csproj
)
StdbModule.csproj必需的.csproj文件(必须命名为StdbModule.csproj
)
StdbModule.csprojxml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SpacetimeDB.Runtime" Version="1.*" />
</ItemGroup>
</Project>xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SpacetimeDB.Runtime" Version="1.*" />
</ItemGroup>
</Project>Prerequisites
前置条件
bash
undefinedbash
undefinedInstall .NET 8 SDK (required, not .NET 9)
安装.NET 8 SDK(必需,不要使用.NET 9)
Install WASI workload
安装WASI工作负载
dotnet workload install wasi-experimental
---dotnet workload install wasi-experimental
---Client SDK
客户端SDK
Installation
安装
bash
dotnet add package SpacetimeDB.ClientSDKbash
dotnet add package SpacetimeDB.ClientSDKGenerate Module Bindings
生成模块绑定
bash
spacetime generate --lang csharp --out-dir module_bindings --module-path PATH_TO_MODULEThis creates , , , and .
SpacetimeDBClient.g.csTables/*.g.csReducers/*.g.csTypes/*.g.csbash
spacetime generate --lang csharp --out-dir module_bindings --module-path PATH_TO_MODULE此命令会生成、、和文件。
SpacetimeDBClient.g.csTables/*.g.csReducers/*.g.csTypes/*.g.csConnection Setup
连接设置
csharp
using SpacetimeDB;
using SpacetimeDB.Types;
var conn = DbConnection.Builder()
.WithUri("http://localhost:3000")
.WithDatabaseName("my-database")
.WithToken(savedToken)
.OnConnect(OnConnected)
.OnConnectError(err => Console.Error.WriteLine($"Failed: {err}"))
.OnDisconnect((conn, err) => { if (err != null) Console.Error.WriteLine(err); })
.Build();
void OnConnected(DbConnection conn, Identity identity, string authToken)
{
// Save authToken to persistent storage for reconnection
Console.WriteLine($"Connected: {identity}");
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.SubscribeToAllTables();
}csharp
using SpacetimeDB;
using SpacetimeDB.Types;
var conn = DbConnection.Builder()
.WithUri("http://localhost:3000")
.WithDatabaseName("my-database")
.WithToken(savedToken)
.OnConnect(OnConnected)
.OnConnectError(err => Console.Error.WriteLine($"连接失败: {err}"))
.OnDisconnect((conn, err) => { if (err != null) Console.Error.WriteLine(err); })
.Build();
void OnConnected(DbConnection conn, Identity identity, string authToken)
{
// 将authToken保存到持久化存储以便重新连接
Console.WriteLine($"已连接: {identity}");
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.SubscribeToAllTables();
}Critical: FrameTick
关键:FrameTick
The SDK does NOT automatically process messages. You must call regularly.
FrameTick()csharp
// Console application
while (running) { conn.FrameTick(); Thread.Sleep(16); }
// Unity: call conn?.FrameTick() in Update()Warning: Do NOT call from a background thread. It modifies and can cause data races.
FrameTick()conn.DbSDK不会自动处理消息。你必须定期调用。
FrameTick()csharp
// 控制台应用
while (running) { conn.FrameTick(); Thread.Sleep(16); }
// Unity:在Update()中调用conn?.FrameTick()警告:不要从后台线程调用。它会修改,可能导致数据竞争。
FrameTick()conn.DbSubscribing to Tables
订阅表
csharp
// SQL queries
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.OnError((ctx, err) => Console.Error.WriteLine($"Subscription failed: {err}"))
.Subscribe(new[] {
"SELECT * FROM player",
"SELECT * FROM message WHERE sender = :sender"
});
// Subscribe to all tables (development only)
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.SubscribeToAllTables();
// Subscription handle for later unsubscribe
SubscriptionHandle handle = conn.SubscriptionBuilder()
.OnApplied(ctx => Console.WriteLine("Applied"))
.Subscribe(new[] { "SELECT * FROM player" });
handle.UnsubscribeThen(ctx => Console.WriteLine("Unsubscribed"));Warning: cannot be mixed with on the same connection.
SubscribeToAllTables()Subscribe()csharp
// SQL查询
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.OnError((ctx, err) => Console.Error.WriteLine($"订阅失败: {err}"))
.Subscribe(new[] {
"SELECT * FROM player",
"SELECT * FROM message WHERE sender = :sender"
});
// 订阅所有表(仅开发环境使用)
conn.SubscriptionBuilder()
.OnApplied(OnSubscriptionApplied)
.SubscribeToAllTables();
// 订阅句柄,用于后续取消订阅
SubscriptionHandle handle = conn.SubscriptionBuilder()
.OnApplied(ctx => Console.WriteLine("已应用"))
.Subscribe(new[] { "SELECT * FROM player" });
handle.UnsubscribeThen(ctx => Console.WriteLine("已取消订阅"));警告:同一连接上不能混合使用和。
SubscribeToAllTables()Subscribe()Accessing the Client Cache
访问客户端缓存
csharp
// Iterate all rows
foreach (var player in ctx.Db.Player.Iter()) { Console.WriteLine(player.Name); }
// Count rows
int playerCount = ctx.Db.Player.Count;
// Find by unique/primary key — returns nullable
Player? player = ctx.Db.Player.Identity.Find(someIdentity);
if (player != null) { Console.WriteLine(player.Name); }
// Filter by BTree index — returns IEnumerable
foreach (var p in ctx.Db.Player.Level.Filter(1)) { }csharp
// 遍历所有行
foreach (var player in ctx.Db.Player.Iter()) { Console.WriteLine(player.Name); }
// 统计行数
int playerCount = ctx.Db.Player.Count;
// 通过唯一/主键查找 — 返回可空类型
Player? player = ctx.Db.Player.Identity.Find(someIdentity);
if (player != null) { Console.WriteLine(player.Name); }
// 通过B树索引过滤 — 返回IEnumerable
foreach (var p in ctx.Db.Player.Level.Filter(1)) { }Row Event Callbacks
行事件回调
csharp
ctx.Db.Player.OnInsert += (EventContext ctx, Player player) => {
Console.WriteLine($"Player joined: {player.Name}");
};
ctx.Db.Player.OnDelete += (EventContext ctx, Player player) => {
Console.WriteLine($"Player left: {player.Name}");
};
ctx.Db.Player.OnUpdate += (EventContext ctx, Player oldRow, Player newRow) => {
Console.WriteLine($"Player {oldRow.Name} renamed to {newRow.Name}");
};
// Checking event source
ctx.Db.Player.OnInsert += (EventContext ctx, Player player) => {
switch (ctx.Event)
{
case Event<Reducer>.SubscribeApplied:
break; // Initial subscription data
case Event<Reducer>.Reducer(var reducerEvent):
Console.WriteLine($"Reducer: {reducerEvent.Reducer}");
break;
}
};csharp
ctx.Db.Player.OnInsert += (EventContext ctx, Player player) => {
Console.WriteLine($"玩家加入: {player.Name}");
};
ctx.Db.Player.OnDelete += (EventContext ctx, Player player) => {
Console.WriteLine($"玩家离开: {player.Name}");
};
ctx.Db.Player.OnUpdate += (EventContext ctx, Player oldRow, Player newRow) => {
Console.WriteLine($"玩家 {oldRow.Name} 更名为 {newRow.Name}");
};
// 检查事件来源
ctx.Db.Player.OnInsert += (EventContext ctx, Player player) => {
switch (ctx.Event)
{
case Event<Reducer>.SubscribeApplied:
break; // 初始订阅数据
case Event<Reducer>.Reducer(var reducerEvent):
Console.WriteLine($"归约器: {reducerEvent.Reducer}");
break;
}
};Calling Reducers
调用归约器
csharp
ctx.Reducers.SendMessage("Hello, world!");
ctx.Reducers.CreatePlayer("NewPlayer");
// Reducer completion callbacks
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}");
};
// Unhandled reducer errors
conn.OnUnhandledReducerError += (ReducerEventContext ctx, Exception ex) => {
Console.Error.WriteLine($"Reducer error: {ex.Message}");
};csharp
ctx.Reducers.SendMessage("Hello, world!");
ctx.Reducers.CreatePlayer("NewPlayer");
// 归约器完成回调
conn.Reducers.OnSendMessage += (ReducerEventContext ctx, string text) => {
if (ctx.Event.Status is Status.Committed)
Console.WriteLine($"消息已发送: {text}");
else if (ctx.Event.Status is Status.Failed(var reason))
Console.Error.WriteLine($"发送失败: {reason}");
};
// 未处理的归约器错误
conn.OnUnhandledReducerError += (ReducerEventContext ctx, Exception ex) => {
Console.Error.WriteLine($"归约器错误: {ex.Message}");
};Identity and Authentication
身份与认证
csharp
// In OnConnect callback — save token for reconnection
void OnConnected(DbConnection conn, Identity identity, string authToken)
{
// Save authToken to persistent storage (file, config, PlayerPrefs, etc.)
SaveToken(authToken);
}
// Reconnect with saved token
string savedToken = LoadToken();
DbConnection.Builder()
.WithUri("http://localhost:3000")
.WithDatabaseName("my-database")
.WithToken(savedToken)
.OnConnect(OnConnected)
.Build();
// Pass null or omit WithToken for anonymous connectioncsharp
// 在OnConnect回调中 — 保存token以便重新连接
void OnConnected(DbConnection conn, Identity identity, string authToken)
{
// 将authToken保存到持久化存储(文件、配置、PlayerPrefs等)
SaveToken(authToken);
}
// 使用保存的token重新连接
string savedToken = LoadToken();
DbConnection.Builder()
.WithUri("http://localhost:3000")
.WithDatabaseName("my-database")
.WithToken(savedToken)
.OnConnect(OnConnected)
.Build();
// 传入null或省略WithToken以匿名连接Commands
命令
bash
spacetime start
spacetime publish <module-name> --module-path <backend-dir>
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
spacetime generate --lang csharp --out-dir <client>/SpacetimeDB --module-path <backend-dir>
spacetime logs <module-name>bash
spacetime start
spacetime publish <module-name> --module-path <backend-dir>
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
spacetime generate --lang csharp --out-dir <client>/SpacetimeDB --module-path <backend-dir>
spacetime logs <module-name>