mcpField
C
csharp-server

csharp-server

github.com/clockworklabs開発25.4k54 回インストール今日更新
リポジトリ

csharp-server について

SpacetimeDB サーバーモジュールを構築するための C# SDK リファレンスです。モジュール開発者向けに、テーブル、Reducer、インデックス、ビュー、スケジュール処理、プロシージャ、HTTP ハンドラーを説明します。

主な機能

  1. 01テーブルと列属性の定義
  2. 02Reducer とライフサイクルフックの実装
  3. 03インデックスによる行の検索と更新
  4. 04ビューと可視性フィルターの作成
  5. 05スケジュールテーブルとプロシージャの設定
  6. 06インバウンドおよびアウトバウンド HTTP の処理

活用シーン

  1. 01SpacetimeDB モジュールのアプリケーションデータ設計
  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 です。このリファレンスでは、すべてを 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マイクロ秒単位の期間
UuidUUID

オプションのカラム: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 });

コメント