---
description: "⛔ MANDATORY: Read this ENTIRE file before writing ANY SpacetimeDB C# code. Contains SDK patterns from official documentation."
globs: **/*.cs
alwaysApply: true
---

# SpacetimeDB C# SDK

## ⛔ COMMON MISTAKES — LLM HALLUCINATIONS

These are **actual errors** observed when LLMs generate SpacetimeDB C# code:

### 1. Wrong Package Name for Server Modules
```csharp
// ❌ WRONG — this package doesn't exist
<PackageReference Include="SpacetimeDB.SDK" Version="1.*" />
<PackageReference Include="SpacetimeDB.ServerSdk" Version="1.*" />

// ✅ CORRECT — use Runtime for server modules
<PackageReference Include="SpacetimeDB.Runtime" Version="1.*" />
```

### 2. Missing `partial` Keyword
```csharp
// ❌ WRONG — missing partial
public struct MyTable { }
public class Module { }

// ✅ CORRECT — must be partial
public partial struct MyTable { }
public static partial class Module { }
```

### 3. Lifecycle Hook Names Starting with "On"
```csharp
// ❌ WRONG — will cause STDB0010 error
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
public static void OnClientConnected(ReducerContext ctx) { }

// ✅ CORRECT — no "On" prefix
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
public static void ClientConnected(ReducerContext ctx) { }
```

### 4. Wrong Timestamp Property Name
```csharp
// ❌ WRONG — property doesn't exist
var micros = timestamp.MicrosecondsSinceEpoch;

// ✅ CORRECT — full name
var micros = timestamp.MicrosecondsSinceUnixEpoch;
```

### 5. Wrong ScheduleAt Syntax
```csharp
// ❌ WRONG — ScheduleAt.Time is not a method
ScheduledAt = ScheduleAt.Time(timestamp)

// ✅ CORRECT — use constructor syntax
ScheduledAt = new ScheduleAt.Time(new Timestamp(microseconds))
// OR with TimeSpan
ScheduledAt = new ScheduleAt.Time(ctx.Timestamp + TimeSpan.FromSeconds(60))
```

### 6. Table Accessor Casing Is Exact
```csharp
// ✅ CORRECT — match the accessor exactly
[SpacetimeDB.Table(Accessor = "user", Public = true)]
public partial class User { ... }
ctx.Db.user.Insert(...);

// ❌ WRONG — incorrect casing
ctx.Db.User.Insert(...); // Accessor is "user", so property is ctx.Db.user
```

### 7. Client Callback Signature Errors
```csharp
// ❌ WRONG — guessing at signatures
.OnDisconnect((conn, ctx, err) => ...)
.OnConnectError((ctx, err) => ...)

// ✅ CORRECT — check actual delegate types
.OnDisconnect((conn, err) => ...)  // DbConnection, Exception?
.OnConnectError(err => ...)         // Exception
```

### 8. WithUri Takes String, Not Uri
```csharp
// ❌ WRONG — Uri object not accepted
.WithUri(new Uri("http://localhost:3000"))

// ✅ CORRECT — use string directly
.WithUri("http://localhost:3000")
```

### 9. Missing RuntimeIdentifier for WASM Build
```xml
<!-- ❌ WRONG — won't produce WASM output -->
<TargetFramework>net8.0</TargetFramework>

<!-- ✅ CORRECT — required for spacetime publish -->
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
```

### 10. Subscribing Before Connected
```csharp
// ❌ WRONG — subscription fails silently
_conn = builder.Build();
_conn.SubscriptionBuilder().SubscribeToAllTables();  // NOT CONNECTED YET!

// ✅ CORRECT — subscribe inside OnConnect callback
private void OnConnected(DbConnection conn, Identity identity, string token)
{
    conn.SubscriptionBuilder()
        .OnApplied(OnSubscriptionApplied)
        .SubscribeToAllTables();
}
```

### 11. Nullable Struct Handling with Find()
```csharp
// ❌ WRONG — accessing properties directly on nullable
if (existing != null)
{
    ctx.Db.User.Id.Update(new User { Id = existing.Id, ... });
}

// ✅ CORRECT — use .Value or pattern matching
if (ctx.Db.User.Id.Find(id) is User user)
{
    ctx.Db.User.Id.Update(user with { Name = newName });
}
```

### 12. Sum Type Syntax Errors
```csharp
// ❌ WRONG — struct instead of record
public partial struct Shape : TaggedEnum<(Circle, Rectangle)> { }

// ❌ WRONG — missing variant names in tuple
public partial record Shape : TaggedEnum<(Circle, Rectangle)> { }

// ✅ CORRECT — record with named tuple elements
public partial record Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }
```

### 13. Index Attribute Ambiguity
```csharp
// ❌ WRONG — ambiguous with System.Index
[Index.BTree(Accessor = "by_name", Columns = new[] { nameof(Name) })]

// ✅ CORRECT — always use full namespace
[SpacetimeDB.Index.BTree(Accessor = "by_name", Columns = new[] { nameof(Name) })]
```

---

## 1) Common Mistakes Table

### Server-side errors

| Wrong | Right | Error |
|-------|-------|-------|
| Missing `partial` keyword | `public partial struct Table` | Generated code won't compile |
| `ctx.Db.User` when `Accessor = "user"` | `ctx.Db.user` | Accessors are case-sensitive |
| `Optional<string>` | `string?` | Type not found |
| `ctx.Db.Table.Get(id)` | `ctx.Db.Table.Id.Find(id)` | Method not found |
| Wrong .csproj name | `StdbModule.csproj` | Publish fails silently |
| .NET 9 SDK | .NET 8 SDK only | WASI compilation fails |
| Missing WASI workload | `dotnet workload install wasi-experimental` | Build fails |
| `[Procedure]` attribute | Reducers only | Procedures not supported in C# |
| Missing `Public = true` | Add to `[Table]` attribute | Clients can't subscribe |
| Using `Random` | Avoid non-deterministic code | Sandbox violation |
| async/await in reducers | Synchronous only | Not supported |
| `[Index.BTree(...)]` | `[SpacetimeDB.Index.BTree(...)]` | Ambiguous with System.Index |
| `Columns = ["A", "B"]` | `Columns = new[] { "A", "B" }` | Collection expressions invalid in attributes |
| `partial struct` or `partial class : TaggedEnum` | `partial record : TaggedEnum` | Sum types must be record |
| `TaggedEnum<(A, B)>` | `TaggedEnum<(A A, B B)>` | Tuple must include variant names |

---

## 2) Table Definition (CRITICAL)

**Tables MUST use `partial struct` or `partial class` for code generation.**

```csharp
using SpacetimeDB;

// ❌ WRONG — missing partial!
[SpacetimeDB.Table(Accessor = "Player")]
public struct Player { }  // Will not generate properly!

// ✅ CORRECT — with partial keyword
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
public partial struct Player
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public ulong Id;

    public Identity OwnerId;
    public string Name;
    public Timestamp CreatedAt;
}

// With single-column index
[SpacetimeDB.Table(Accessor = "Task", Public = true)]
public partial struct Task
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public ulong Id;

    [SpacetimeDB.Index.BTree]
    public Identity OwnerId;

    public string Title;
    public bool Completed;
}

// Multi-column index (use fully-qualified attribute!)
[SpacetimeDB.Table(Accessor = "Score", Public = true)]
[SpacetimeDB.Index.BTree(Accessor = "by_player_game", Columns = new[] { "PlayerId", "GameId" })]
public partial struct Score
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public ulong Id;

    public Identity PlayerId;
    public string GameId;
    public int Points;
}
```

### Field attributes

```csharp
[SpacetimeDB.PrimaryKey]     // Exactly one per table (required)
[SpacetimeDB.AutoInc]        // Auto-increment (integer fields only)
[SpacetimeDB.Unique]         // Unique constraint
[SpacetimeDB.Index.BTree]    // Single-column B-tree index
[SpacetimeDB.Default(value)] // Default value for new columns
```

### Column types

```csharp
byte, sbyte, short, ushort   // 8/16-bit integers
int, uint, long, ulong       // 32/64-bit integers
float, double                // Floats
bool                         // Boolean
string                       // Text
Identity                     // User identity
Timestamp                    // Timestamp
ScheduleAt                   // For scheduled tables
T?                           // Nullable (e.g., string?)
List<T>                      // Arrays
```

### Insert with auto-increment

```csharp
// Insert returns the row with generated ID
var player = ctx.Db.Player.Insert(new Player
{
    Id = 0,  // Pass 0 to trigger auto-increment
    OwnerId = ctx.Sender,
    Name = name,
    CreatedAt = ctx.Timestamp
});
ulong newId = player.Id;  // Get actual generated ID
```

---

## 3) Module and Reducers

**The Module class MUST be `public static partial class`.**

```csharp
using SpacetimeDB;

public static partial class Module
{
    [SpacetimeDB.Reducer]
    public static void CreateTask(ReducerContext ctx, string title)
    {
        // Validate
        if (string.IsNullOrEmpty(title))
        {
            throw new Exception("Title cannot be empty");  // Rolls back transaction
        }

        // Insert
        ctx.Db.Task.Insert(new Task
        {
            Id = 0,
            OwnerId = ctx.Sender,
            Title = title,
            Completed = false
        });
    }

    [SpacetimeDB.Reducer]
    public static void CompleteTask(ReducerContext ctx, ulong taskId)
    {
        if (ctx.Db.Task.Id.Find(taskId) is not Task task)
        {
            throw new Exception("Task not found");
        }

        if (task.OwnerId != ctx.Sender)
        {
            throw new Exception("Not authorized");
        }

        ctx.Db.Task.Id.Update(task with { Completed = true });
    }

    [SpacetimeDB.Reducer]
    public static void DeleteTask(ReducerContext ctx, ulong taskId)
    {
        ctx.Db.Task.Id.Delete(taskId);
    }
}
```

### Update Pattern (CRITICAL)

```csharp
// ✅ CORRECT — use `with` expression or provide complete row
if (ctx.Db.Task.Id.Find(id) is Task task)
{
    ctx.Db.Task.Id.Update(task with { Title = newTitle });
}

// ❌ WRONG — partial update nulls out other fields!
ctx.Db.Task.Id.Update(new Task { Id = id, Title = newTitle });
```

### Lifecycle reducers

```csharp
public static partial class Module
{
    [SpacetimeDB.Reducer(ReducerKind.Init)]
    public static void Init(ReducerContext ctx)
    {
        // Called once when module is first published
        Log.Info("Module initialized");
    }

    [SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
    public static void ClientConnected(ReducerContext ctx)
    {
        // ctx.Sender is the connecting client
        Log.Info($"Client connected: {ctx.Sender}");
    }

    [SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)]
    public static void ClientDisconnected(ReducerContext ctx)
    {
        // Clean up client state
        Log.Info($"Client disconnected: {ctx.Sender}");
    }
}
```

### ReducerContext API

```csharp
ctx.Sender          // Identity of the caller
ctx.Timestamp       // Current timestamp
ctx.Db              // Database access
ctx.Identity        // Module's own identity
ctx.ConnectionId    // Connection ID (nullable)
```

---

## 4) Database Access

### Primary key operations

```csharp
// Find by primary key — returns nullable, use pattern matching
if (ctx.Db.Task.Id.Find(taskId) is Task task)
{
    // Use task
}

// Update by primary key
ctx.Db.Task.Id.Update(updatedTask);

// Delete by primary key
ctx.Db.Task.Id.Delete(taskId);
```

### Index operations

```csharp
// Find by unique index — returns nullable
if (ctx.Db.Player.Username.Find("alice") is Player player)
{
    // Found player
}

// Filter by B-tree index — returns iterator
foreach (var task in ctx.Db.Task.OwnerId.Filter(ctx.Sender))
{
    // Process each task
}
```

### Iterate all rows

```csharp
// Full table scan
foreach (var task in ctx.Db.Task.Iter())
{
    // Process each task
}
```

---

## 5) Custom Types

**Use `[SpacetimeDB.Type]` for custom structs/enums. Must be `partial`.**

```csharp
using SpacetimeDB;

[SpacetimeDB.Type]
public partial struct Position
{
    public int X;
    public int Y;
}

[SpacetimeDB.Type]
public partial struct PlayerStats
{
    public int Health;
    public int Mana;
    public Position Location;
}

// Use in table
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
public partial struct Player
{
    [SpacetimeDB.PrimaryKey]
    public Identity Id;

    public string Name;
    public PlayerStats Stats;
}
```

---

## 6) Sum Types / Tagged Enums (CRITICAL)

**Sum types MUST use `partial record` (not `partial class`) and inherit from `TaggedEnum<T>`.**

```csharp
using SpacetimeDB;

// Step 1: Define variant types as partial structs with [Type]
[SpacetimeDB.Type]
public partial struct Circle { public int Radius; }

[SpacetimeDB.Type]
public partial struct Rectangle { public int Width; public int Height; }

// Step 2: Define sum type as partial RECORD (not struct or class!) inheriting TaggedEnum
// The tuple MUST include both the type AND a name for each variant
[SpacetimeDB.Type]
public partial record Shape : TaggedEnum<(Circle Circle, Rectangle Rectangle)> { }

// Step 3: Use in a table
[SpacetimeDB.Table(Accessor = "Drawing", Public = true)]
public partial struct Drawing
{
    [SpacetimeDB.PrimaryKey]
    public int Id;
    public Shape ShapeA;
    public Shape ShapeB;
}
```

### Creating sum type values

```csharp
// Create variant instances using the generated nested types
var circle = new Shape.Circle(new Circle { Radius = 10 });
var rect = new Shape.Rectangle(new Rectangle { Width = 4, Height = 6 });

// Insert into table
ctx.Db.Drawing.Insert(new Drawing { Id = 1, ShapeA = circle, ShapeB = rect });
```

---

## 7) Client SDK Setup

```csharp
// Connection pattern
_conn = DbConnection.Builder()
    .WithUri("http://localhost:3000")  // String, NOT Uri object!
    .WithModuleName("my-module")
    .WithToken(savedToken)  // null for first connection
    .OnConnect(OnConnected)
    .OnDisconnect((conn, err) => { })
    .OnConnectError(err => { })
    .Build();

// Subscribe in OnConnected callback, NOT before!
private void OnConnected(DbConnection conn, Identity identity, string token)
{
    conn.SubscriptionBuilder()
        .OnApplied(OnSubscriptionApplied)
        .SubscribeToAllTables();
}
```

### ⚠️ CRITICAL: FrameTick

**You MUST call `FrameTick()` regularly** — without it, no callbacks fire:

```csharp
while (running)
{
    conn.FrameTick();
    Thread.Sleep(16);  // ~60 times per second
}
```

### Row Callbacks (Client-side)

```csharp
// Register callbacks BEFORE subscribing
_conn.Db.Task.OnInsert += (EventContext ctx, Task row) =>
{
    // Row was inserted
};

_conn.Db.Task.OnUpdate += (EventContext ctx, Task oldRow, Task newRow) =>
{
    // Row was updated
};

_conn.Db.Task.OnDelete += (EventContext ctx, Task row) =>
{
    // Row was deleted
};
```

### Invoking Reducers (Client-side)

```csharp
// Reducers are called as methods on conn.Reducers
_conn.Reducers.CreateTask("My task");

// Register callback for reducer completion
_conn.Reducers.OnCreateTask += (ctx) =>
{
    if (ctx.Event.Status is Status.Committed)
    {
        // Success
    }
    else if (ctx.Event.Status is Status.Failed failed)
    {
        // Failed: failed.Item contains error
    }
};
```

---

## 8) Scheduled Tables

For C# scheduled tables, `ScheduledAt = ...` must point to a field of type `ScheduleAt` on the scheduled table.

```csharp
using SpacetimeDB;

[SpacetimeDB.Table(Accessor = "Reminder", Scheduled = nameof(SendReminder))]
public partial struct Reminder
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public ulong Id;

    public string Message;
    public ScheduleAt ScheduledAt;
}

public static partial class Module
{
    // Scheduled reducer receives the full row
    [SpacetimeDB.Reducer]
    public static void SendReminder(ReducerContext ctx, Reminder reminder)
    {
        Log.Info($"Reminder: {reminder.Message}");
        // Row is automatically deleted after reducer completes
    }

    [SpacetimeDB.Reducer]
    public static void CreateReminder(ReducerContext ctx, string message, ulong delaySecs)
    {
        var futureTime = ctx.Timestamp + TimeSpan.FromSeconds(delaySecs);
        ctx.Db.Reminder.Insert(new Reminder
        {
            Id = 0,
            Message = message,
            ScheduledAt = new ScheduleAt.Time(futureTime)
        });
    }

    [SpacetimeDB.Reducer]
    public static void CancelReminder(ReducerContext ctx, ulong reminderId)
    {
        ctx.Db.Reminder.Id.Delete(reminderId);
    }
}
```

---

## 9) Logging

```csharp
using SpacetimeDB;

Log.Debug("Debug message");
Log.Info("Information");
Log.Warn("Warning");
Log.Error("Error occurred");
Log.Panic("Critical failure");  // Terminates execution
```

---

## 10) Timestamps

### Server-side

```csharp
// Use ctx.Timestamp for current time
ctx.Db.Task.Insert(new Task
{
    // ...
    CreatedAt = ctx.Timestamp
});

// Never use DateTime.Now - it's non-deterministic!
```

### Client-side

```csharp
// Timestamp has MicrosecondsSinceUnixEpoch property
var dateTime = DateTimeOffset.FromUnixTimeMilliseconds(
    row.CreatedAt.MicrosecondsSinceUnixEpoch / 1000
).LocalDateTime;
```

---

## 11) Project Setup

### Required .csproj (MUST be named `StdbModule.csproj`)

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="SpacetimeDB.Runtime" Version="1.*" />
  </ItemGroup>
</Project>
```

### Prerequisites

```bash
# Install .NET 8 SDK (required, not .NET 9)
# Download from https://dotnet.microsoft.com/download/dotnet/8.0

# Install WASI workload
dotnet workload install wasi-experimental
```

---

## 12) Commands

```bash
# Start local server
spacetime start

# Publish module
spacetime publish <module-name> --module-path <backend-dir>

# Clear database and republish
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>

# Generate bindings
spacetime generate --lang csharp --out-dir <client>/SpacetimeDB --module-path <backend-dir>

# View logs
spacetime logs <module-name>
```

---

## 13) Hard Requirements

**C#-specific:**

1. **Tables and Module MUST be `partial`** — required for code generation
2. **Use PascalCase for table access** — `ctx.Db.TableName`, not `ctx.Db.tableName`
3. **Project file MUST be named `StdbModule.csproj`** — CLI requirement
4. **Requires .NET 8 SDK** — .NET 9 and newer not yet supported
5. **Install WASI workload** — `dotnet workload install wasi-experimental`
6. **C# does NOT support procedures** — use reducers only
7. **Reducers must be deterministic** — no filesystem, network, timers, or `Random`
8. **Add `Public = true`** — if clients need to subscribe to a table
9. **Use `T?` for nullable fields** — not `Optional<T>`
10. **Pass `0` for auto-increment** — to trigger ID generation on insert
11. **MUST call `FrameTick()` regularly** — client callbacks won't fire otherwise
12. **DO NOT edit generated bindings** — regenerate with `spacetime generate`
