csharp-server について
SpacetimeDB サーバーモジュールを構築するための C# SDK リファレンスです。モジュール開発者向けに、テーブル、Reducer、インデックス、ビュー、スケジュール処理、プロシージャ、HTTP ハンドラーを説明します。
主な機能
- 01テーブルと列属性の定義
- 02Reducer とライフサイクルフックの実装
- 03インデックスによる行の検索と更新
- 04ビューと可視性フィルターの作成
- 05スケジュールテーブルとプロシージャの設定
- 06インバウンドおよびアウトバウンド HTTP の処理
活用シーン
- 01SpacetimeDB モジュールのアプリケーションデータ設計
- 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 です。このリファレンスでは、すべてを using SpacetimeDB; だけで済む単一の public static partial class Module にまとめています。
Reducer、procedure、view、HTTP ハンドラー、router など、SpacetimeDB 属性を通じてエクスポートするメソッドは 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 | ユーザー ID |
ConnectionId | 接続ハンドル |
Timestamp | サーバーのタイムスタンプ(エポックからのマイクロ秒) |
TimeDuration | マイクロ秒単位の期間 |
Uuid | UUID |
オプションのカラム:nullable 型(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; ... }
1つのカラムでフィルタリングしてループするより、複数カラムのインデックスを使用してください。
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) { ... }
DB 操作
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 は、接続ライフサイクルの Reducer 内も含めて ConnectionId? です。null 非許容カラムに保存したり、インデックスアクセサーに渡したりする前に、値を確認するか null 非許容に変換してください。
View
// 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();
}
View のコンテキストでは、読み取り専用のテーブルハンドルが公開されます。これらは Count に加え、宣言済みのインデックスに対する Find/Filter をサポートしますが、テーブル全体の Iter() はサポートしません。手続き型 View の走査は、インデックス検索またはフィルターから開始してください。
クエリビルダー形式の View では、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)
);
手続き型 View の主キーは、属性で宣言します:[SpacetimeDB.View(Accessor = "CatalogEntry", Public = true, PrimaryKey = nameof(CatalogRow.Sku))]。
手続き型 View の主キーは、スキーマメタデータとして明示的に指定します。View 自体に主キーを公開する必要がある場合にのみ追加してください。ソーステーブルの主キーは View に継承されません。
B-tree の範囲検索ではタプルを使用します。例: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)は、送信者 ID、時刻、乱数を取得する唯一の手段です。モジュールでは標準ライブラリの時計および乱数生成器は使用できません。
// 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
スケジュールされたテーブル
nameof(...) が解決できるよう、スケジュールされたテーブルとそのリデューサーを同じ Module クラスで宣言してください。
[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 });
スケジュールされたリデューサーのコールバックには、通常の [SpacetimeDB.Reducer] 属性を使用します。ReducerKind.Scheduled はありません。テーブルの Scheduled オプションでコールバックを関連付けます。
128 ビットの数値表現から ConnectionId を構築するには、正確に 16 バイトのリトルエンディアン形式でエンコードし、ConnectionId.From(bytes) を呼び出します。戻り値は nullable なので、使用する前にチェックする必要があります。
プロシージャと HTTP
プロシージャは不安定な 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 };
送信 HTTP は ctx.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 と修飾してください。
スケジュールされたプロシージャには、通常のスケジュールされたテーブルの形式を使用します。その Scheduled の名前は、ProcedureContext とスケジュールされた行を受け取る [SpacetimeDB.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 });