Skip to main content

csharp-server

SpacetimeDB C# server module SDK reference. Use when writing tables, reducers, or module logic in C#.

Informations de source

Dépôt
clockworklabs/SpacetimeDB
Dernière activité de la source
5 novembre 2026 à 16:16
Langue détectée de SKILL.md
anglais
Étoiles
25 531
Forks
1 117

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
csharp-server
description
SpacetimeDB C# server module SDK reference. Use when writing tables, reducers, or module logic in C#.
license
Apache-2.0
metadata
{"author":"clockworklabs","version":"2.0","role":"server","language":"csharp","cursor_globs":"**/*.cs","cursor_always_apply":true}
# SpacetimeDB C# SDK Reference ## Build Configuration (.NET 10) New .NET 10 modules build as managed libraries by default. Use the CLI for WASM builds, or select the runtime identifier explicitly when invoking dotnet: ```sh spacetime init my-project --lang csharp --dotnet-version 10 # From the generated module directory: dotnet build # managed DLL dotnet publish -c Release -r wasi-wasm # NativeAOT-LLVM WASM spacetime build # CLI selects WASM automatically ``` - Do not set an unconditional `RuntimeIdentifier=wasi-wasm` in .NET 10 projects. Keep WASM-only settings such as `PublishTrimmed`, `SelfContained`, and `MSBuildEnableWorkloadResolver=false` conditional on that RID. - The module must reference both `Microsoft.DotNet.ILCompiler.LLVM` and `runtime.$(NETCoreSdkPortableRuntimeIdentifier).Microsoft.DotNet.ILCompiler.LLVM` (version `10.0.0-*`) conditionally on `RuntimeIdentifier=wasi-wasm`. The runtime package no longer supplies these dependencies transitively. Keep the dotnet-experimental NuGet feed configured for WASM restores. - `spacetime build` and `spacetime publish` select WASM automatically. .NET 10 does not require `--native-aot` or an AOT environment variable. - Allow restore when switching between managed and WASM builds; do not reuse `--no-restore` across modes. For multi-targeted projects, select `-f net10.0` when publishing and restrict the LLVM 10 package references to .NET 10. - .NET 8 behavior is unchanged: WASM/JIT by default, with `--native-aot` selecting NativeAOT where supported. The current flag is `SPACETIMEDB_WASM_AOT`; the CLI also sets/clears `EXPERIMENTAL_WASM_AOT` for older projects. Do not manually define the C# compiler symbol to choose a .NET 10 build mode. - Module dependencies use ordinary `ProjectReference` or packaged managed assemblies. .NET 10 composition uses generated assembly descriptors, not the dependency's standalone WASM exports; no special root/library build flag is needed. ## Module Structure Reducers are static methods in a `static partial class`; tables are `public partial struct`s. This reference keeps everything in one `public static partial class Module`, which needs only `using SpacetimeDB;`: Methods exported through SpacetimeDB attributes, including reducers, procedures, views, HTTP handlers, and routers, must be `public static`; generated bindings invoke them from another class. ```csharp 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 }); } } ``` ## Tables `[SpacetimeDB.Table(...)]` on a `public partial struct`. `Accessor` should be PascalCase: ```csharp [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; } ``` Options: `Accessor = "PascalCase"` (recommended), `Public = true`, `Scheduled = nameof(ReducerFn)`, `ScheduledAt = nameof(field)`, `Event = true` `ctx.Db` accessors use the `Accessor` name: `ctx.Db.Entity`, `ctx.Db.ScoreRecord`. ## Column Types | C# type | Notes | |---------|-------| | `byte` / `ushort` / `uint` / `ulong` | unsigned integers | | `U128` / `U256` | large unsigned integers (SpacetimeDB types) | | `sbyte` / `short` / `int` / `long` | signed integers | | `I128` / `I256` | large signed integers (SpacetimeDB types) | | `float` / `double` | floats | | `bool` | boolean | | `string` | text | | `List<T>` | list/array | | `Identity` | user identity | | `ConnectionId` | connection handle | | `Timestamp` | server timestamp (microseconds since epoch) | | `TimeDuration` | duration in microseconds | | `Uuid` | UUID | Optional columns: nullable types (`string? Nickname`, `uint? HighScore`) ## Column Attributes The complete set of column attributes: ```csharp [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 ``` Defaults support compatible addition of a newly appended field. Do not apply `[Default(...)]` to primary-key, unique, or auto-increment fields. ## Indexes Write the index attribute fully qualified: `[SpacetimeDB.Index.BTree]`. Prefer inline for single-column; multi-column uses struct-level: ```csharp // 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; ... } ``` Prefer a multi-column index over filtering by one column and looping. ## Reducers ```csharp [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 Operations ```csharp 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 ``` Note: Filter/Iter return enumerables. Use `.ToList()` if you need to sort or mutate. The pattern is `ctx.Db.{Accessor}.{ColumnName}.{Method}(value)` for all indexed column operations. ## Lifecycle Hooks ```csharp [SpacetimeDB.Reducer(ReducerKind.Init)] public static void Init(ReducerContext ctx) { ... } [SpacetimeDB.Reducer(ReducerKind.ClientConnected)] public static void ClientConnected(ReducerContext ctx) { ... } [SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)] public static void ClientDisconnected(ReducerContext ctx) { ... } ``` Reducer and procedure method names must not start with `On`, `on`, or `__`; the code generator rejects them (STDB0010), so name lifecycle reducers `ClientConnected`, not `OnConnect`. `ctx.ConnectionId` is `ConnectionId?`, including in connection lifecycle reducers. Check or unwrap it before storing it in a non-nullable column or passing it to an index accessor. ## Views ```csharp // 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 contexts expose read-only table handles. These support `Count` plus `Find`/`Filter` on declared indexes, but not full-table `Iter()`. Start procedural view traversal from an indexed lookup or filter. Query-builder views use `ViewContext`, `ctx.From`, and return `IQuery<T>` directly. Use `Where` for predicates and `RightSemijoin` when the result should contain right-side rows that have a matching left-side row: ```csharp ctx.From.Article().Where(article => article.Published.Eq(true)); ctx.From.Subscription().RightSemijoin( ctx.From.Account(), (subscription, account) => subscription.AccountId.Eq(account.Id) ); ``` Declare a procedural view primary key in its attribute: `[SpacetimeDB.View(Accessor = "CatalogEntry", Public = true, PrimaryKey = nameof(CatalogRow.Sku))]`. Procedural-view primary keys are explicit schema metadata. Add one only when the view itself is required to expose a primary key; a source table's primary key is not inherited by the view. Inclusive btree ranges use tuples, for example `ctx.Db.Shipment.DeliverBy.Filter((new Timestamp(1_000), new Timestamp(2_000)))`. Views can also accept arguments (2.13+), after the context: ```csharp [SpacetimeDB.View(Accessor = "EntityById", Public = true)] public static Entity? EntityById(AnonymousViewContext ctx, ulong id) => ctx.Db.Entity.Id.Find(id); ``` Floating-point arguments and primary keys are rejected on argument-bearing views. See client skills for SQL subscription syntax and restrictions. ## Client Visibility Filters ```csharp [ClientVisibilityFilter] public static readonly Filter PrivateNoteFilter = new Filter.Sql( "SELECT * FROM OwnedRow WHERE Owner = :sender" ); ``` ## Reducer Context API `ReducerContext` (`ctx`) is the only source of sender identity, time, and randomness; stdlib clocks and RNG are unavailable in modules. ```csharp // 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 ``` ## Submodules C# assembly composition requires SpacetimeDB/Runtime 2.13+, .NET 10, and C# 14 in the root and dependencies. NativeAOT-LLVM module builds require Windows or Linux, not macOS. Reference module assemblies via `ProjectReference` or NuGet. If libraries live beneath the root project, exclude their source from the root compile glob (`<Compile Remove="libraries/**/*.cs" />`). Dependencies, including transitive ones, register once in `public` unless some module mounts them with a `Namespace` attribute: ```csharp // Root Lib.cs, before namespace/type declarations: [assembly: SpacetimeDB.Namespace(typeof(AuthLib.Marker), Accessor = "MyAuth", Name = "auth_data")] ``` The marker is any accessible type in the dependency assembly. Root code uses `ctx.Db.MyAuth.User` and `ctx.From.MyAuth.User()`; library code keeps `ctx.Db.User` and `ctx.From.User()`. To call a library helper, pass `ctx.As.MyAuth` (`tx.As.MyAuth` inside `WithTx`; 2.14+). Passing `ctx` directly works only while the library has one unambiguous instance; otherwise it throws `InvalidOperationException`. Helpers share the caller's transaction; namespaces are not helper security boundaries. `Accessor` controls C# members; `Name` pins the canonical namespace used in SQL (`auth_data.<table>`). Without `Name`, the root's case policy derives it (`MyAuth` → `my_auth` by default). Changing the canonical namespace drops its tables and their data; keep `Name` fixed to rename only accessors. Named dependencies retain their naming policy; `public` dependencies inherit the root's and cannot explicitly conflict with it. In 2.14+, any module can mount its own dependencies, and the same assembly can be mounted more than once with independent tables at each path; 2.13 allows only root mounts, once per assembly. Cross-language composition is unsupported. Named dependencies cannot declare lifecycle reducers, nonempty environments, or RLS filters. Define RLS in the root with canonical qualified table names. Named-submodule routes are ignored; root handlers may delegate to library helpers. Dependencies in `public` can contribute lifecycle reducers (subject to uniqueness), environment declarations, and routes. ## Environment Variables ```csharp [SpacetimeDB.Env] public partial struct EnvironmentSchema { public string API_KEY; [SpacetimeDB.EnvValues("development", "production")] public string MODE; public string? LOG_LEVEL; } // In a root entry point: // ctx.Env.API_KEY: string; ctx.Env.LOG_LEVEL: string? // ctx.Env.Get("LOG_LEVEL"): string? ``` Enable nullable annotations for optional `string?` fields. One `[SpacetimeDB.Env]` struct declares the environment; `[EnvValues(...)]` restricts allowed strings. Colliding accessor names remain available through `Get("KEY")`. See the concepts skill for shared environment and secret-handling rules. ## Scheduled Tables Declare the scheduled table and its reducer in the same `Module` class so `nameof(...)` resolves: ```csharp [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) { // a one-time row is deleted after this runs; an interval row stays } // 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 }); ``` Scheduled reducer callbacks use the ordinary `[SpacetimeDB.Reducer]` attribute. There is no `ReducerKind.Scheduled`; the table's `Scheduled` option associates the callback. To let only the scheduler run a callback, reject calls where `ctx.Sender != ctx.DatabaseIdentity`. To construct a `ConnectionId` from a 128-bit numeric representation, encode it as exactly 16 little-endian bytes and call `ConnectionId.From(bytes)`. The result is nullable and must be checked before use. ## Procedures and HTTP Procedures are unstable APIs, so modules using them should include `#pragma warning disable STDB_UNSTABLE`. They receive `ProcedureContext` and may return `[SpacetimeDB.Type]` values: ```csharp [SpacetimeDB.Type] public partial struct ResultValue { public string Value; } [SpacetimeDB.Procedure] public static ResultValue Inspect(ProcedureContext ctx, string input) => new() { Value = input }; ```
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub