mcpField
C
csharp-server

csharp-server

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

关于 csharp-server

面向模块开发者的 SpacetimeDB C# SDK 参考,说明如何构建服务器模块,涵盖表、Reducer、索引、视图、定时任务、过程和 HTTP 处理器。

主要功能

  1. 01定义表和列属性
  2. 02实现 Reducer 与生命周期钩子
  3. 03通过索引查询和更新行
  4. 04创建视图与可见性筛选器
  5. 05配置定时表和过程
  6. 06处理入站与出站 HTTP

适用场景

  1. 01为 SpacetimeDB 模块设计应用数据
  2. 02实现数据库变更与生命周期逻辑
  3. 03通过视图提供筛选或派生数据
  4. 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以微秒为单位的时长
UuidUUID

可选列:可空类型(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 });

评论

支持 Markdown