关于 csharp-server
面向模块开发者的 SpacetimeDB C# SDK 参考,说明如何构建服务器模块,涵盖表、Reducer、索引、视图、定时任务、过程和 HTTP 处理器。
主要功能
- 01定义表和列属性
- 02实现 Reducer 与生命周期钩子
- 03通过索引查询和更新行
- 04创建视图与可见性筛选器
- 05配置定时表和过程
- 06处理入站与出站 HTTP
适用场景
- 01为 SpacetimeDB 模块设计应用数据
- 02实现数据库变更与生命周期逻辑
- 03通过视图提供筛选或派生数据
- 04添加定时任务或 HTTP 端点
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ clockworklabs/ spacetimedb --skill csharp-server安装后在 Claude Code 中输入/csharp-server确认已加载。
详细介绍
SpacetimeDB C# SDK 参考
模块结构
Reducer 是 static partial class 中的静态方法;表是 public partial struct。本参考将所有内容放在一个 public static partial class Module 中,只需 using SpacetimeDB;:
通过 SpacetimeDB 属性导出的方法,包括 Reducer、过程、视图、HTTP 处理程序和路由器,都必须是 public static;生成的绑定会从另一个类中调用它们。
using SpacetimeDB;
public static partial class Module
{
[SpacetimeDB.Table(Accessor = "ScoreRecord", Public = true)]
public partial struct ScoreRecord
{
[PrimaryKey]
[AutoInc]
public ulong Id;
public Identity Owner;
public uint Value;
}
[SpacetimeDB.Reducer]
public static void AddRecord(ReducerContext ctx, uint value)
{
ctx.Db.ScoreRecord.Insert(new ScoreRecord { Id = 0, Owner = ctx.Sender, Value = value });
}
}
表
在 public partial struct 上使用 [SpacetimeDB.Table(...)]。Accessor 应使用 PascalCase:
[SpacetimeDB.Table(Accessor = "Entity", Public = true)]
public partial struct Entity
{
[PrimaryKey]
[AutoInc]
public ulong Id;
[SpacetimeDB.Index.BTree]
public Identity Owner;
public string Name;
[SpacetimeDB.Index.BTree]
public bool Active;
}
选项:Accessor = "PascalCase"(推荐)、Public = true、Scheduled = nameof(ReducerFn)、ScheduledAt = nameof(field)、Event = true
ctx.Db 访问器使用 Accessor 名称:ctx.Db.Entity、ctx.Db.ScoreRecord。
列类型
| C# 类型 | 说明 |
|---|---|
byte / ushort / uint / ulong | 无符号整数 |
U128 / U256 | 大型无符号整数(SpacetimeDB 类型) |
sbyte / short / int / long | 有符号整数 |
I128 / I256 | 大型有符号整数(SpacetimeDB 类型) |
float / double | 浮点数 |
bool | 布尔值 |
string | 文本 |
List<T> | 列表/数组 |
Identity | 用户身份 |
ConnectionId | 连接句柄 |
Timestamp | 服务端时间戳(自纪元以来的微秒数) |
TimeDuration | 以微秒为单位的时长 |
Uuid | UUID |
可选列:可空类型(string? Nickname、uint? HighScore)
列属性
完整的列属性如下:
[PrimaryKey] // primary key
[AutoInc] // auto-increment (use 0 as placeholder on insert)
[Unique] // unique constraint; indexes the column, enables .Find()
[SpacetimeDB.Index.BTree] // btree index (enables .Filter() on this column)
[Default(true)] // migration-safe default for a newly appended field
默认值支持以兼容方式新增追加字段。不要对主键、唯一或自增字段应用 [Default(...)]。
索引
完整限定索引属性:[SpacetimeDB.Index.BTree]。单列索引优先内联声明;多列索引使用结构体级声明:
// Inline (preferred for single-column):
[SpacetimeDB.Index.BTree]
public ulong AuthorId;
// Access: ctx.Db.Post.AuthorId.Filter(authorId)
// Multi-column (struct-level):
[SpacetimeDB.Table(Accessor = "Membership")]
[SpacetimeDB.Index.BTree(Accessor = "ByGroupUser", Columns = new[] { nameof(GroupId), nameof(UserId) })]
public partial struct Membership { public ulong GroupId; public Identity UserId; ... }
相比按单列筛选后再循环,优先使用多列索引。
Reducer
[SpacetimeDB.Reducer]
public static void CreateEntity(ReducerContext ctx, string name, int age)
{
ctx.Db.Entity.Insert(new Entity { Owner = ctx.Sender, Name = name, Age = age, Active = true });
}
// No arguments:
[SpacetimeDB.Reducer]
public static void DoReset(ReducerContext ctx) { ... }
数据库操作
var row = ctx.Db.Entity.Insert(new Entity { Name = "Sample" }); // Insert; returns the row with AutoInc fields assigned
ctx.Db.Entity.Id.Find(entityId); // Find by PK → Entity? (nullable)
ctx.Db.Entity.Identity.Find(ctx.Sender); // Find by unique column → Entity?
if (ctx.Db.Entity.Id.Find(entityId) is { } entity) { ... } // unwrap Entity? before member access
ctx.Db.Item.AuthorId.Filter(authorId); // Filter by index → IEnumerable<Item>
ctx.Db.Entity.Iter(); // All rows → IEnumerable<Entity>
ctx.Db.Entity.Count; // Count rows
ctx.Db.Entity.Id.Update(existing with { Name = newName }); // Update by PK
ctx.Db.Entity.Id.Delete(entityId); // Delete by PK
注意:Filter/Iter 会返回可枚举对象。如果需要排序或修改,请使用 .ToList()。
所有索引列操作都采用 ctx.Db.{Accessor}.{ColumnName}.{Method}(value) 模式。
生命周期钩子
[SpacetimeDB.Reducer(ReducerKind.Init)]
public static void OnInit(ReducerContext ctx) { ... }
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
public static void OnConnect(ReducerContext ctx) { ... }
[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)]
public static void OnDisconnect(ReducerContext ctx) { ... }
ctx.ConnectionId 的类型是 ConnectionId?,包括在连接生命周期 Reducer 中。将其存入非空列或传给索引访问器之前,请先检查或解包。
视图
// Anonymous view (same result for all clients):
[SpacetimeDB.View(Accessor = "ActiveUsers", Public = true)]
public static List<Entity> ActiveUsers(AnonymousViewContext ctx)
{
return ctx.Db.Entity.Active.Filter(true).ToList();
}
// Per-user view:
[SpacetimeDB.View(Accessor = "MyEntities", Public = true)]
public static List<Entity> MyEntities(ViewContext ctx)
{
return ctx.Db.Entity.Owner.Filter(ctx.Sender).ToList();
}
视图上下文会公开只读的表句柄。这些句柄支持 Count,以及对已声明索引使用 Find/Filter,但不支持全表 Iter()。请从索引查找或筛选开始遍历过程式视图。
查询构建器视图使用 ViewContext 和 ctx.From,并直接返回 IQuery<T>。使用 Where 指定谓词;当结果应包含与某个左侧行匹配的右侧行时,使用 RightSemijoin:
ctx.From.Article().Where(article => article.Published.Eq(true));
ctx.From.Subscription().RightSemijoin(
ctx.From.Account(),
(subscription, account) => subscription.AccountId.Eq(account.Id)
);
在过程式视图的属性中声明主键:[SpacetimeDB.View(Accessor = "CatalogEntry", Public = true, PrimaryKey = nameof(CatalogRow.Sku))]。
过程式视图的主键是显式的架构元数据。仅当视图本身需要公开主键时才添加;源表的主键不会继承到视图中。
btree 闭区间使用元组,例如 ctx.Db.Shipment.DeliverBy.Filter((new Timestamp(1_000), new Timestamp(2_000)))。
客户端可见性筛选器
[ClientVisibilityFilter]
public static readonly Filter PrivateNoteFilter = new Filter.Sql(
"SELECT * FROM OwnedRow WHERE Owner = :sender"
);
Reducer 上下文 API
ReducerContext(ctx)是发送者身份、时间和随机数的唯一来源;模块中无法使用标准库时钟和随机数生成器。
// Auth: ctx.Sender is the caller's Identity
if (row.Owner != ctx.Sender)
throw new Exception("unauthorized");
// Server timestamp (deterministic per reducer call)
ctx.Db.Item.Insert(new Item { CreatedAt = ctx.Timestamp, .. });
// Timestamp arithmetic
var expiry = ctx.Timestamp + new TimeDuration(delayMicros);
// Deterministic RNG
int roll = ctx.Rng.Next(1, 7); // [1, 7): inclusive 1, exclusive 7
double f = ctx.Rng.NextDouble(); // [0.0, 1.0)
// Timestamp → milliseconds since epoch
timestamp.MicrosecondsSinceUnixEpoch / 1000
定时表
在同一个 Module 类中声明定时表及其 reducer,这样 nameof(...) 才能正确解析:
[SpacetimeDB.Table(
Accessor = "TickTimer",
Scheduled = nameof(Tick),
ScheduledAt = nameof(ScheduledAt),
Public = true
)]
public partial struct TickTimer
{
[PrimaryKey]
[AutoInc]
public ulong ScheduledId;
public ScheduleAt ScheduledAt;
}
[SpacetimeDB.Reducer]
public static void Tick(ReducerContext ctx, TickTimer timer)
{
// timer row is auto-deleted after this reducer runs
}
// One-time: fires once at a specific time
var at = new ScheduleAt.Time(ctx.Timestamp + new TimeDuration(10_000_000));
// Repeating: fires on an interval
var at = new ScheduleAt.Interval(TimeSpan.FromSeconds(5));
ctx.Db.TickTimer.Insert(new TickTimer { ScheduledId = 0, ScheduledAt = at });
定时 reducer 回调使用普通的 [SpacetimeDB.Reducer] 特性。没有 ReducerKind.Scheduled;表的 Scheduled 选项会关联该回调。
要从 128 位数值表示构造 ConnectionId,请将其编码为恰好 16 个小端字节,然后调用 ConnectionId.From(bytes)。返回值可为空,使用前必须检查。
Procedures 和 HTTP
Procedures 是不稳定 API,因此使用它们的模块应包含 #pragma warning disable STDB_UNSTABLE。它们接收 ProcedureContext,并且可以返回 [SpacetimeDB.Type] 值:
[SpacetimeDB.Type]
public partial struct ResultValue { public string Value; }
[SpacetimeDB.Procedure]
public static ResultValue Inspect(ProcedureContext ctx, string input) =>
new() { Value = input };
可通过 ctx.Http 发送出站 HTTP 请求;使用响应前,请处理其成功/错误结果。使用 ctx.WithTx 打开短时数据库事务。应在打开事务前执行网络 I/O,并且只在回调中进行数据库操作。
var result = ctx.Http.Get(uri);
var text = result.Match(
response => response.Body.ToStringUtf8Lossy(),
error => throw new Exception(error.Message)
);
ctx.WithTx(tx =>
{
tx.Db.ScoreRecord.Insert(new ScoreRecord { Id = 0, Owner = ctx.Sender, Value = 1 });
return 0;
});
WithTx 是泛型的:其回调必须返回一个值(如果不需要结果,请使用 return 0;)。回调生成的 tx.Db 会公开模块表访问器;ProcedureContext 和 HandlerContext 不会直接公开表。
对于非 GET 请求,请直接构造并发送 HttpRequest:
var result = ctx.Http.Send(new HttpRequest
{
Uri = uri,
Method = SpacetimeDB.HttpMethod.Post,
Headers = new() { new HttpHeader("content-type", "text/plain") },
Body = HttpBody.FromString(payload),
});
HttpResponse.StatusCode 的类型是 ushort。HTTP 标头是 HttpHeader 值,而非元组;每个标头的 Value 都是 byte[];请使用 System.Text.Encoding.UTF8.GetString(header.Value) 解码文本值。将请求正文视为字节:使用 HttpBody.FromString(...) 创建文本,使用 new HttpBody(bytes) 提供原始字节,使用 ToBytes() 读取原始字节,使用 ToStringUtf8Lossy() 读取文本。不存在 HttpBody.FromBytes,且 ToString() 不会返回正文内容。当 .NET 的隐式 System.Net.Http 导入可能导致名称冲突时,请使用 SpacetimeDB.HttpMethod 的完整限定名。
定时 procedures 使用普通的定时表结构。其中的 Scheduled 名称指向一个 [SpacetimeDB.Procedure] 方法,该方法接收 ProcedureContext 和定时行;该 procedure 内的数据库访问通过 ctx.WithTx 进行。
入站 HTTP 使用处理程序特性和一个路由器。处理程序的数据库访问也通过 ctx.WithTx 进行:
[SpacetimeDB.HttpHandler]
public static HttpResponse Health(HandlerContext ctx, HttpRequest request) => new(
200, HttpVersion.Http11, new(), HttpBody.FromString("ok")
);
[SpacetimeDB.HttpRouter]
public static Router Routes() => SpacetimeDB.Router.New().Get("/health", Handlers.Health);
Handlers 是根据标记了 [SpacetimeDB.HttpHandler] 的方法生成的;请勿自行声明。在路由器中引用带特性的方法时,请使用 Handlers.MethodName。
自定义类型
[SpacetimeDB.Type]
public enum Status { Online, Away, Offline }
[SpacetimeDB.Type]
public partial struct Point { public float X; public float Y; }
带标签的枚举(判别联合):使用空主体且不带构造函数参数的 partial record。负载是 [Type] partial struct:
[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 : SpacetimeDB.TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }
// Construct variants via the generated nested constructors:
var a = new Shape.Circle(new Circle { Radius = 10 });
var b = new Shape.Rectangle(new Rectangle { Width = 4, Height = 6 });