diff --git a/docs/index.md b/docs/index.md index 864f7327..9180ff68 100644 --- a/docs/index.md +++ b/docs/index.md @@ -216,7 +216,7 @@ or at a specific time. ::: :::server-csharp While SpacetimeDB doesn't support nested transactions, -a reducer can [schedule another reducer](/docs/modules/c-sharp#scheduler-tables) to run at an interval, +a reducer can [schedule another reducer](/docs/modules/c-sharp#scheduled-reducers) to run at an interval, or at a specific time. ::: diff --git a/docs/sdks/c-sharp/index.md b/docs/sdks/c-sharp/index.md index e9c5f23a..16fd2068 100644 --- a/docs/sdks/c-sharp/index.md +++ b/docs/sdks/c-sharp/index.md @@ -1,56 +1,22 @@ # The SpacetimeDB C# client SDK -The SpacetimeDB client C# for Rust contains all the tools you need to build native clients for SpacetimeDB modules using C#. - -## Table of Contents - -- [The SpacetimeDB C# client SDK](#the-spacetimedb-c-client-sdk) - - [Table of Contents](#table-of-contents) - - [Install the SDK](#install-the-sdk) - - [Using the `dotnet` CLI tool](#using-the-dotnet-cli-tool) - - [Using Unity](#using-unity) - - [Generate module bindings](#generate-module-bindings) - - [Initialization](#initialization) - - [Property `SpacetimeDBClient.instance`](#property-spacetimedbclientinstance) - - [Class `NetworkManager`](#class-networkmanager) - - [Method `SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect) - - [Event `SpacetimeDBClient.onIdentityReceived`](#event-spacetimedbclientonidentityreceived) - - [Event `SpacetimeDBClient.onConnect`](#event-spacetimedbclientonconnect) - - [Subscribe to queries](#subscribe-to-queries) - - [Method `SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe) - - [Event `SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied) - - [Method \[`SpacetimeDBClient.OneOffQuery`\]](#method-spacetimedbclientoneoffquery) - - [View rows of subscribed tables](#view-rows-of-subscribed-tables) - - [Class `{TABLE}`](#class-table) - - [Static Method `{TABLE}.Iter`](#static-method-tableiter) - - [Static Method `{TABLE}.FilterBy{COLUMN}`](#static-method-tablefilterbycolumn) - - [Static Method `{TABLE}.FindBy{COLUMN}`](#static-method-tablefindbycolumn) - - [Static Method `{TABLE}.Count`](#static-method-tablecount) - - [Static Event `{TABLE}.OnInsert`](#static-event-tableoninsert) - - [Static Event `{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete) - - [Static Event `{TABLE}.OnDelete`](#static-event-tableondelete) - - [Static Event `{TABLE}.OnUpdate`](#static-event-tableonupdate) - - [Observe and invoke reducers](#observe-and-invoke-reducers) - - [Class `Reducer`](#class-reducer) - - [Static Method `Reducer.{REDUCER}`](#static-method-reducerreducer) - - [Static Event `Reducer.On{REDUCER}`](#static-event-reduceronreducer) - - [Class `ReducerEvent`](#class-reducerevent) - - [Enum `Status`](#enum-status) - - [Variant `Status.Committed`](#variant-statuscommitted) - - [Variant `Status.Failed`](#variant-statusfailed) - - [Variant `Status.OutOfEnergy`](#variant-statusoutofenergy) - - [Identity management](#identity-management) - - [Class `AuthToken`](#class-authtoken) - - [Static Method `AuthToken.Init`](#static-method-authtokeninit) - - [Static Property `AuthToken.Token`](#static-property-authtokentoken) - - [Static Method `AuthToken.SaveToken`](#static-method-authtokensavetoken) - - [Class `Identity`](#class-identity) - - [Customizing logging](#customizing-logging) - - [Interface `ISpacetimeDBLogger`](#interface-ispacetimedblogger) - - [Class `ConsoleLogger`](#class-consolelogger) - - [Class `UnityDebugLogger`](#class-unitydebuglogger) - -## Install the SDK +The SpacetimeDB client for C# contains all the tools you need to build native clients for SpacetimeDB modules using C#. + +| Name | Description | +|---------------------------------------------------------|---------------------------------------------------------------------------| +| [Project setup](#project-setup) | Configure a C# project to use the SpacetimeDB C# client SDK. | +| [Generate module bindings](#generate-module-bindings) | Use the SpacetimeDB CLI to generate module-specific types and interfaces. | +| [`DbConnection` type](#type-dbconnection) | A connection to a remote database. | +| [`IDbContext` interface](#interface-idbcontext) | Methods for interacting with the remote database. | +| [`EventContext` type](#type-eventcontext) | Implements [`IDbContext`](##interface-idbcontext) for [row callbacks](#callback-oninsert). | +| [`ReducerEventContext` type](#type-reducereventcontext) | Implements [`IDbContext`](##interface-idbcontext) for [reducer callbacks](#observe-and-invoke-reducers). | +| [`SubscriptionEventContext` type](#type-subscriptioneventcontext) | Implements [`IDbContext`](##interface-idbcontext) for [subscription callbacks](#subscribe-to-queries). | +| [`ErrorContext` type](#type-errorcontext) | Implements [`IDbContext`](##interface-idbcontext) for error-related callbacks. | +| [Access the client cache](#access-the-client-cache) | Access to your local view of the database. | +| [Observe and invoke reducers](#observe-and-invoke-reducers) | Send requests to the database to run reducers, and register callbacks to run when notified of reducers. | +| [Identify a client](#identify-a-client) | Types for identifying users and client connections. | + +## Project setup ### Using the `dotnet` CLI tool @@ -81,853 +47,878 @@ spacetime generate --lang cs --out-dir module_bindings --project-path PATH-TO-MO Replace `PATH-TO-MODULE-DIRECTORY` with the path to your SpacetimeDB module. -## Initialization +## Type `DbConnection` -### Property `SpacetimeDBClient.instance` +A connection to a remote database is represented by the `DbConnection` class. This class is generated per module and contains information about the types, tables, and reducers defined by your module. -```cs -namespace SpacetimeDB { +| Name | Description | +|------------------------------------------------------------------------|-------------------------------------------------------------------------------| +| [Connect to a module](#connect-to-a-module) | Construct a `DbConnection` instance. | +| [Advance the connection](#advance-the-connection-and-process-messages) | Poll the `DbConnection` or run it in the background. | +| [Access tables and reducers](#access-tables-and-reducers) | Access the client cache, request reducer invocations, and register callbacks. | -public class SpacetimeDBClient { - public static SpacetimeDBClient instance; -} +## Connect to a module +```csharp +class DbConnection +{ + public static DbConnectionBuilder Builder(); } ``` -This is the global instance of a SpacetimeDB client in a particular .NET/Unity process. Much of the SDK is accessible through this instance. +Construct a `DbConnection` by calling `DbConnection.Builder()`, chaining configuration methods, and finally calling `.Build()`. At a minimum, you must specify `WithUri` to provide the URI of the SpacetimeDB instance, and `WithModuleName` to specify the module's name or identity. -### Class `NetworkManager` +| Name | Description | +|---------------------------------------------------------|--------------------------------------------------------------------------------------------| +| [WithUri method](#method-withuri) | Set the URI of the SpacetimeDB instance hosting the remote database. | +| [WithModuleName method](#method-withmodulename) | Set the name or identity of the remote module. | +| [OnConnect callback](#callback-onconnect) | Register a callback to run when the connection is successfully established. | +| [OnConnectError callback](#callback-onconnecterror) | Register a callback to run if the connection is rejected or the host is unreachable. | +| [OnDisconnect callback](#callback-ondisconnect) | Register a callback to run when the connection ends. | +| [WithToken method](#method-withtoken) | Supply a token to authenticate with the remote database. | +| [Build method](#method-build) | Finalize configuration and open the connection. | -The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in the scene. Click on the GameManager object in the scene, and in the inspector, add the `NetworkManager` component. +### Method `WithUri` -This component will handle updating and closing the [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect), you still need to handle that yourself. See the [Unity Tutorial](/docs/unity) for more information. +```csharp +class DbConnectionBuilder +{ + public DbConnectionBuilder WithUri(Uri uri); +} +``` -### Method `SpacetimeDBClient.Connect` +Configure the URI of the SpacetimeDB instance or cluster which hosts the remote module. -```cs -namespace SpacetimeDB { +### Method `WithModuleName` -class SpacetimeDBClient { - public void Connect( - string? token, - string host, - string addressOrName, - bool sslEnabled = true - ); +```csharp +class DbConnectionBuilder +{ + public DbConnectionBuilder WithModuleName(string nameOrIdentity); } +``` + +Configure the SpacetimeDB domain name or `Identity` of the remote module which identifies it within the SpacetimeDB instance or cluster. +### Callback `OnConnect` + +```csharp +class DbConnectionBuilder +{ + public DbConnectionBuilder OnConnect(Action callback); } ``` - +Chain a call to `.OnConnect(callback)` to your builder to register a callback to run when your new `DbConnection` successfully initiates its connection to the remote module. The callback accepts three arguments: a reference to the `DbConnection`, the `Identity` by which SpacetimeDB identifies this connection, and a private access token which can be saved and later passed to [`WithToken`](#method-withtoken) to authenticate the same user in future connections. -Connect to a database named `addressOrName` accessible over the internet at the URI `host`. +### Callback `OnConnectError` -| Argument | Type | Meaning | -| --------------- | --------- | -------------------------------------------------------------------------- | -| `token` | `string?` | Identity token to use, if one is available. | -| `host` | `string` | URI of the SpacetimeDB instance running the module. | -| `addressOrName` | `string` | Address or name of the module. | -| `sslEnabled` | `bool` | Whether or not to use SSL when connecting to SpacetimeDB. Default: `true`. | +```csharp +class DbConnectionBuilder +{ + public DbConnectionBuilder OnConnectError(Action callback); +} +``` -If a `token` is supplied, it will be passed to the new connection to identify and authenticate the user. Otherwise, a new token and [`Identity`](#class-identity) will be generated by the server and returned in [`onConnect`](#event-spacetimedbclientonconnect). +Chain a call to `.OnConnectError(callback)` to your builder to register a callback to run when your connection fails. -```cs -using SpacetimeDB; -using SpacetimeDB.Types; +A known bug in the SpacetimeDB Rust client SDK currently causes this callback never to be invoked. [`OnDisconnect`](#callback-ondisconnect) callbacks are invoked instead. -const string DBNAME = "chat"; +### Callback `OnDisconnect` + +```csharp +class DbConnectionBuilder +{ + public DbConnectionBuilder OnDisconnect(Action callback); +} +``` -// Connect to a local DB with a fresh identity -SpacetimeDBClient.instance.Connect(null, "localhost:3000", DBNAME, false); +Chain a call to `.OnDisconnect(callback)` to your builder to register a callback to run when your `DbConnection` disconnects from the remote module, either as a result of a call to [`Disconnect`](#method-disconnect) or due to an error. -// Connect to cloud with a fresh identity -SpacetimeDBClient.instance.Connect(null, "dev.spacetimedb.net", DBNAME, true); +### Method `WithToken` -// Connect to cloud using a saved identity from the filesystem, or get a new one and save it -AuthToken.Init(); -Identity localIdentity; -SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); -SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { - AuthToken.SaveToken(authToken); - localIdentity = identity; +```csharp +class DbConnectionBuilder +{ + public DbConnectionBuilder WithToken(string token = null); } ``` -(You should probably also store the returned `Identity` somewhere; see the [`onIdentityReceived`](#event-spacetimedbclientonidentityreceived) event.) +Chain a call to `.WithToken(token)` to your builder to provide an OpenID Connect compliant JSON Web Token to authenticate with, or to explicitly select an anonymous connection. If this method is not called or `None` is passed, SpacetimeDB will generate a new `Identity` and sign a new private access token for the connection. -### Event `SpacetimeDBClient.onIdentityReceived` +### Method `Build` -```cs -namespace SpacetimeDB { - -class SpacetimeDBClient { - public event Action onIdentityReceived; +```csharp +class DbConnectionBuilder +{ + public DbConnection Build(); } +``` + +After configuring the connection and registering callbacks, attempt to open the connection. + +## Advance the connection and process messages + +In the interest of supporting a wide variety of client applications with different execution strategies, the SpacetimeDB SDK allows you to choose when the `DbConnection` spends compute time and processes messages. If you do not arrange for the connection to advance by calling one of these methods, the `DbConnection` will never advance, and no callbacks will ever be invoked. +| Name | Description | +|---------------------------------------------|-------------------------------------------------------| +| [`FrameTick` method](#method-frametick) | Process messages on the main thread without blocking. | + +#### Method `FrameTick` + +```csharp +class DbConnection { + public void FrameTick(); } ``` -Called when we receive an auth token, [`Identity`](#class-identity) and `Address` from the server. The [`Identity`](#class-identity) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The `Address` is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity). +`FrameTick` will advance the connection until no work remains or until it is disconnected, then return rather than blocking. Games might arrange for this message to be called every frame. -To store the auth token to the filesystem, use the static method [`AuthToken.SaveToken`](#static-method-authtokensavetoken). You may also want to store the returned [`Identity`](#class-identity) in a local variable. +It is not advised to run `FrameTick` on a background thread, since it modifies [`dbConnection.Db`](#property-db). If main thread code is also accessing the `Db`, it may observe data races when `FrameTick` runs on another thread. -If an existing auth token is used to connect to the database, the same auth token and the identity it came with will be returned verbatim in `onIdentityReceived`. +(Note that the SDK already does most of the work for parsing messages on a background thread. `FrameTick()` does the minimal amount of work needed to apply updates to the `Db`.) -```cs -// Connect to cloud using a saved identity from the filesystem, or get a new one and save it -AuthToken.Init(); -Identity localIdentity; -SpacetimeDBClient.instance.Connect(AuthToken.Token, "dev.spacetimedb.net", DBNAME, true); -SpacetimeDBClient.instance.onIdentityReceived += (string authToken, Identity identity, Address address) { - AuthToken.SaveToken(authToken); - localIdentity = identity; +## Access tables and reducers + +### Property `Db` + +```csharp +class DbConnection +{ + public RemoteTables Db; + /* other members */ } ``` -### Event `SpacetimeDBClient.onConnect` +The `Db` property of the `DbConnection` provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache). -```cs -namespace SpacetimeDB { +### Property `Reducers` -class SpacetimeDBClient { - public event Action onConnect; +```csharp +class DbConnection +{ + public RemoteReducers Reducers; + /* other members */ } +``` +The `Reducers` field of the `DbConnection` provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers). + +## Interface `IDbContext` + +```csharp +interface IDbContext +{ + /* methods */ } ``` -Allows registering delegates to be invoked upon authentication with the database. +[`DbConnection`](#type-dbconnection), [`EventContext`](#type-eventcontext), [`ReducerEventContext`](#type-reducereventcontext), [`SubscriptionEventContext`](#type-subscriptioneventcontext) and [`ErrorContext`](#type-errorcontext) all implement `IDbContext`. `IDbContext` has methods for inspecting and configuring your connection to the remote database. -Once this occurs, the SDK is prepared for calls to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe). +The `IDbContext` interface is implemented by connections and contexts to *every* module - hence why it takes [`DbView`](#method-db) and [`RemoteReducers`](#method-reducers) as type parameters. -## Subscribe to queries +| Name | Description | +|---------------------------------------------------------------|--------------------------------------------------------------------------| +| [`IRemoteDbContext` interface](#interface-iremotedbcontext) | Module-specific `IDbContext`. | +| [`Db` method](#method-db) | Provides access to the subscribed view of the remote database's tables. | +| [`Reducers` method](#method-reducers) | Provides access to reducers exposed by the remote module. | +| [`Disconnect` method](#method-disconnect) | End the connection. | +| [Subscribe to queries](#subscribe-to-queries) | Register SQL queries to receive updates about matching rows. | +| [Read connection metadata](#read-connection-metadata) | Access the connection's `Identity` and `ConnectionId` | -### Method `SpacetimeDBClient.Subscribe` +### Interface `IRemoteDbContext` -```cs -namespace SpacetimeDB { +Each module's `module_bindings` exports an interface `IRemoteDbContext` which inherits from `IDbContext`, with the type parameters `DbView` and `RemoteReducers` bound to the types defined for that module. This can be more convenient when creating functions that can be called from any callback for a specific module, but which access the database or invoke reducers, and so must know the type of the `DbView` or `Reducers`. -class SpacetimeDBClient { - public void Subscribe(List queries); -} +### Method `Db` +```csharp +interface IRemoteDbContext +{ + public DbView Db { get; } } ``` -| Argument | Type | Meaning | -| --------- | -------------- | ---------------------------- | -| `queries` | `List` | SQL queries to subscribe to. | +`Db` will have methods to access each table defined by the module. -Subscribe to a set of queries, to be notified when rows which match those queries are altered. +#### Example -`Subscribe` will return an error if called before establishing a connection with the [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect) function. In that case, the queries are not registered. - -The `Subscribe` method does not return data directly. `spacetime generate` will generate classes [`SpacetimeDB.Types.{TABLE}`](#class-table) for each table in your module. These classes are used to reecive information from the database. See the section [View Rows of Subscribed Tables](#view-rows-of-subscribed-tables) for more information. +```csharp +var conn = ConnectToDB(); -A new call to `Subscribe` will remove all previous subscriptions and replace them with the new `queries`. If any rows matched the previous subscribed queries but do not match the new queries, those rows will be removed from the client cache, and [`{TABLE}.OnDelete`](#static-event-tableoninsert) callbacks will be invoked for them. +// Get a handle to the User table +var tableHandle = conn.Db.User; +``` -```cs -using SpacetimeDB; -using SpacetimeDB.Types; +### Method `Reducers` -void Main() +```csharp +interface IRemoteDbContext { - AuthToken.Init(); + public RemoteReducers Reducers { get; } +} +``` - SpacetimeDBClient.instance.onConnect += OnConnect; +`Reducers` will have methods to invoke each reducer defined by the module, +plus methods for adding and removing callbacks on each of those reducers. - // Our module contains a table named "Loot" - Loot.OnInsert += Loot_OnInsert; +#### Example - SpacetimeDBClient.instance.Connect(/* ... */); -} +```csharp +var conn = ConnectToDB(); -void OnConnect() -{ - SpacetimeDBClient.instance.Subscribe(new List { - "SELECT * FROM Loot" - }); -} +// Register a callback to be run every time the SendMessage reducer is invoked +conn.Reducers.OnSendMessage += Reducer_OnSendMessageEvent; +``` + +### Method `Disconnect` -void Loot_OnInsert( - Loot loot, - ReducerEvent? event -) { - Console.Log($"Loaded loot {loot.itemType} at coordinates {loot.position}"); +```csharp +interface IRemoteDbContext +{ + public void Disconnect(); } ``` -### Event `SpacetimeDBClient.onSubscriptionApplied` +Gracefully close the `DbConnection`. Throws an error if the connection is already closed. -```cs -namespace SpacetimeDB { +### Subscribe to queries -class SpacetimeDBClient { - public event Action onSubscriptionApplied; -} +| Name | Description | +|---------------------------------------------------------|-------------------------------------------------------------| +| [`SubscriptionBuilder` type](#type-subscriptionbuilder) | Builder-pattern constructor to register subscribed queries. | +| [`SubscriptionHandle` type](#type-subscriptionhandle) | Manage an active subscripion. | -} -``` +#### Type `SubscriptionBuilder` -Register a delegate to be invoked when a subscription is registered with the database. +| Name | Description | +|----------------------------------------------------------------------------------|-----------------------------------------------------------------| +| [`ctx.SubscriptionBuilder()` constructor](#constructor-ctxsubscriptionbuilder) | Begin configuring a new subscription. | +| [`OnApplied` callback](#callback-onapplied) | Register a callback to run when matching rows become available. | +| [`OnError` callback](#callback-onerror) | Register a callback to run if the subscription fails. | +| [`Subscribe` method](#method-subscribe) | Finish configuration and subscribe to one or more SQL queries. | +| [`SubscribeToAllTables` method](#method-subscribetoalltables) | Convenience method to subscribe to the entire database. | -```cs -using SpacetimeDB; +##### Constructor `ctx.SubscriptionBuilder()` -void OnSubscriptionApplied() +```csharp +interface IRemoteDbContext { - Console.WriteLine("Now listening on queries."); + public SubscriptionBuilder SubscriptionBuilder(); } +``` + +Subscribe to queries by calling `ctx.SubscriptionBuilder()` and chaining configuration methods, then calling `.Subscribe(queries)`. -void Main() +##### Callback `OnApplied` + +```csharp +class SubscriptionBuilder { - // ...initialize... - SpacetimeDBClient.instance.onSubscriptionApplied += OnSubscriptionApplied; + public SubscriptionBuilder OnApplied(Action callback); } ``` -### Method [`SpacetimeDBClient.OneOffQuery`] +Register a callback to run when the subscription is applied and the matching rows are inserted into the client cache. -You may not want to subscribe to a query, but instead want to run a query once and receive the results immediately via a `Task` result: +##### Callback `OnError` ```csharp -// Query all Messages from the sender "bob" -SpacetimeDBClient.instance.OneOffQuery("WHERE sender = \"bob\""); +class SubscriptionBuilder +{ + public SubscriptionBuilder OnError(Action callback); +} ``` -## View rows of subscribed tables +Register a callback to run if the subscription is rejected or unexpectedly terminated by the server. This is most frequently caused by passing an invalid query to [`Subscribe`](#method-subscribe). -The SDK maintains a local view of the database called the "client cache". This cache contains whatever rows are selected via a call to [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe). These rows are represented in the SpacetimeDB .Net SDK as instances of [`SpacetimeDB.Types.{TABLE}`](#class-table). -ONLY the rows selected in a [`SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe) call will be available in the client cache. All operations in the client sdk operate on these rows exclusively, and have no information about the state of the rest of the database. +##### Method `Subscribe` -In particular, SpacetimeDB does not support foreign key constraints. This means that if you are using a column as a foreign key, SpacetimeDB will not automatically bring in all of the rows that key might reference. You will need to manually subscribe to all tables you need information from. - -To optimize network performance, prefer selecting as few rows as possible in your [`Subscribe`](#method-spacetimedbclientsubscribe) query. Processes that need to view the entire state of the database are better run inside the database -- that is, inside modules. +```csharp +class SubscriptionBuilder +{ + public SubscriptionHandle Subscribe(string[] querySqls); +} +``` -### Class `{TABLE}` +Subscribe to a set of queries. `queries` should be an array of SQL query strings. -For each table defined by a module, `spacetime generate` will generate a class [`SpacetimeDB.Types.{TABLE}`](#class-table) whose name is that table's name converted to `PascalCase`. The generated class contains a property for each of the table's columns, whose names are the column names converted to `camelCase`. It also contains various static events and methods. +See [the SpacetimeDB SQL Reference](/docs/sql#subscriptions) for information on the queries SpacetimeDB supports as subscriptions. -Static Methods: +##### Method `SubscribeToAllTables` -- [`{TABLE}.Iter()`](#static-method-tableiter) iterates all subscribed rows in the client cache. -- [`{TABLE}.FilterBy{COLUMN}(value)`](#static-method-tablefilterbycolumn) filters subscribed rows in the client cache by a column value. -- [`{TABLE}.FindBy{COLUMN}(value)`](#static-method-tablefindbycolumn) finds a subscribed row in the client cache by a unique column value. -- [`{TABLE}.Count()`](#static-method-tablecount) counts the number of subscribed rows in the client cache. +```csharp +class SubscriptionBuilder +{ + public void SubscribeToAllTables(); +} +``` -Static Events: +Subscribe to all rows from all public tables. This method is provided as a convenience for simple clients. The subscription initiated by `SubscribeToAllTables` cannot be canceled after it is initiated. You should [`subscribe` to specific queries](#method-subscribe) if you need fine-grained control over the lifecycle of your subscriptions. -- [`{TABLE}.OnInsert`](#static-event-tableoninsert) is called when a row is inserted into the client cache. -- [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete) is called when a row is about to be removed from the client cache. -- If the table has a primary key attribute, [`{TABLE}.OnUpdate`](#static-event-tableonupdate) is called when a row is updated. -- [`{TABLE}.OnDelete`](#static-event-tableondelete) is called while a row is being removed from the client cache. You should almost always use [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete) instead. +#### Type `SubscriptionHandle` -Note that it is not possible to directly insert into the database from the client SDK! All insertion validation should be performed inside serverside modules for security reasons. You can instead [invoke reducers](#observe-and-invoke-reducers), which run code inside the database that can insert rows for you. +A `SubscriptionHandle` represents a subscribed query or a group of subscribed queries. -#### Static Method `{TABLE}.Iter` +The `SubscriptionHandle` does not contain or provide access to the subscribed rows. Subscribed rows of all subscriptions by a connection are contained within that connection's [`ctx.Db`](#property-db). See [Access the client cache](#access-the-client-cache). -```cs -namespace SpacetimeDB.Types { +| Name | Description | +|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| [`IsEnded` property](#property-isended) | Determine whether the subscription has ended. | +| [`IsActive` property](#property-isactive) | Determine whether the subscription is active and its matching rows are present in the client cache. | +| [`Unsubscribe` method](#method-unsubscribe) | Discard a subscription. | +| [`UnsubscribeThen` method](#method-unsubscribethen) | Discard a subscription, and register a callback to run when its matching rows are removed from the client cache. | -class TABLE { - public static IEnumerable Iter(); -} +##### Property `IsEnded` +```csharp +class SubscriptionHandle +{ + public bool IsEnded; } ``` -Iterate over all the subscribed rows in the table. This method is only available after [`SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied) has occurred. +True if this subscription has been terminated due to an unsubscribe call or an error. -When iterating over rows and filtering for those containing a particular column, [`{TABLE}.FilterBy{COLUMN}`](#static-method-tablefilterbycolumn) and [`{TABLE}.FindBy{COLUMN}`](#static-method-tablefindbycolumn) will be more efficient, so prefer those when possible. +##### Property `IsActive` -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { - SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); -}; -SpacetimeDBClient.instance.onSubscriptionApplied += () => { - // Will print a line for each `User` row in the database. - foreach (var user in User.Iter()) { - Console.WriteLine($"User: {user.Name}"); - } -}; -SpacetimeDBClient.instance.connect(/* ... */); +```csharp +class SubscriptionHandle +{ + public bool IsActive; +} ``` -#### Static Method `{TABLE}.FilterBy{COLUMN}` - -```cs -namespace SpacetimeDB.Types { +True if this subscription has been applied and has not yet been unsubscribed. -class TABLE { - public static IEnumerable
FilterBySender(COLUMNTYPE value); -} +##### Method `Unsubscribe` +```csharp +class SubscriptionHandle +{ + public void Unsubscribe(); } ``` -For each column of a table, `spacetime generate` generates a static method on the [table class](#class-table) to filter subscribed rows where that column matches a requested value. +Terminate this subscription, causing matching rows to be removed from the client cache. Any rows removed from the client cache this way will have [`OnDelete` callbacks](#callback-ondelete) run for them. -These methods are named `filterBy{COLUMN}`, where `{COLUMN}` is the column name converted to `PascalCase`. The method's return type is an `IEnumerable` over the [table class](#class-table). +Unsubscribing is an asynchronous operation. Matching rows are not removed from the client cache immediately. Use [`UnsubscribeThen`](#method-unsubscribethen) to run a callback once the unsubscribe operation is completed. -#### Static Method `{TABLE}.FindBy{COLUMN}` +Returns an error if the subscription has already ended, either due to a previous call to `Unsubscribe` or [`UnsubscribeThen`](#method-unsubscribethen), or due to an error. -```cs -namespace SpacetimeDB.Types { - -class TABLE { - // If the column has a #[unique] or #[primarykey] constraint - public static TABLE? FindBySender(COLUMNTYPE value); -} +##### Method `UnsubscribeThen` +```csharp +class SubscriptionHandle +{ + public void UnsubscribeThen(Action? onEnded); } ``` -For each unique column of a table (those annotated `#[unique]` or `#[primarykey]`), `spacetime generate` generates a static method on the [table class](#class-table) to seek a subscribed row where that column matches a requested value. +Terminate this subscription, and run the `onEnded` callback when the subscription is ended and its matching rows are removed from the client cache. Any rows removed from the client cache this way will have [`OnDelete` callbacks](#callback-ondelete) run for them. -These methods are named `findBy{COLUMN}`, where `{COLUMN}` is the column name converted to `PascalCase`. Those methods return a single instance of the [table class](#class-table) if a row is found, or `null` if no row matches the query. +Returns an error if the subscription has already ended, either due to a previous call to [`Unsubscribe`](#method-unsubscribe) or `UnsubscribeThen`, or due to an error. -#### Static Method `{TABLE}.Count` +### Read connection metadata -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public static int Count(); -} +#### Property `Identity` +```csharp +interface IDbContext +{ + public Identity? Identity { get; } } ``` -Return the number of subscribed rows in the table, or 0 if there is no active connection. +Get the `Identity` with which SpacetimeDB identifies the connection. This method returns null if the connection was initiated anonymously and the newly-generated `Identity` has not yet been received, i.e. if called before the [`OnConnect` callback](#callback-onconnect) is invoked. -```cs -using SpacetimeDB; -using SpacetimeDB.Types; +#### Property `ConnectionId` -SpacetimeDBClient.instance.onConnect += (string authToken, Identity identity) => { - SpacetimeDBClient.instance.Subscribe(new List { "SELECT * FROM User" }); -}; -SpacetimeDBClient.instance.onSubscriptionApplied += () => { - Console.WriteLine($"There are {User.Count()} users in the database."); -}; -SpacetimeDBClient.instance.connect(/* ... */); +```csharp +interface IDbContext +{ + public ConnectionId ConnectionId { get; } +} ``` -#### Static Event `{TABLE}.OnInsert` +Get the [`ConnectionId`](#type-connectionid) with which SpacetimeDB identifies the connection. -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void InsertEventHandler( - TABLE insertedValue, - ReducerEvent? dbEvent - ); - public static event InsertEventHandler OnInsert; -} +#### Property `IsActive` +```csharp +interface IDbContext +{ + public bool IsActive { get; } } ``` -Register a delegate for when a subscribed row is newly inserted into the database. +`true` if the connection has not yet disconnected. Note that a connection `IsActive` when it is constructed, before its [`OnConnect` callback](#callback-onconnect) is invoked. -The delegate takes two arguments: +## Type `EventContext` -- A [`{TABLE}`](#class-table) instance with the data of the inserted row -- A [`ReducerEvent?`], which contains the data of the reducer that inserted the row, or `null` if the row is being inserted while initializing a subscription. +An `EventContext` is an [`IDbContext`](#interface-idbcontext) augmented with an [`Event`](#record-event) property. `EventContext`s are passed as the first argument to row callbacks [`OnInsert`](#callback-oninsert), [`OnDelete`](#callback-ondelete) and [`OnUpdate`](#callback-onupdate). -```cs -using SpacetimeDB; -using SpacetimeDB.Types; +| Name | Description | +|-------------------------------------------|---------------------------------------------------------------| +| [`Event` property](#property-event) | Enum describing the cause of the current row callback. | +| [`Db` property](#property-db) | Provides access to the client cache. | +| [`Reducers` property](#property-reducers) | Allows requesting reducers run on the remote database. | +| [`Event` record](#record-event) | Possible events which can cause a row callback to be invoked. | -/* initialize, subscribe to table User... */ +### Property `Event` -User.OnInsert += (User user, ReducerEvent? reducerEvent) => { - if (reducerEvent == null) { - Console.WriteLine($"New user '{user.Name}' received during subscription update."); - } else { - Console.WriteLine($"New user '{user.Name}' inserted by reducer {reducerEvent.Reducer}."); - } -}; +```csharp +class EventContext { + public readonly Event Event; + /* other fields */ +} ``` -#### Static Event `{TABLE}.OnBeforeDelete` +The [`Event`](#record-event) contained in the `EventContext` describes what happened to cause the current row callback to be invoked. -```cs -namespace SpacetimeDB.Types { - -class TABLE { - public delegate void DeleteEventHandler( - TABLE deletedValue, - ReducerEvent dbEvent - ); - public static event DeleteEventHandler OnBeforeDelete; -} +### Property `Db` +```csharp +class EventContext { + public RemoteTables Db; + /* other fields */ } ``` -Register a delegate for when a subscribed row is about to be deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked for each of those rows before any of them is deleted. +The `Db` property of the context provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache). + +### Field `Reducers` -The delegate takes two arguments: +```csharp +class EventContext { + public RemoteReducers Reducers; + /* other fields */ +} +``` -- A [`{TABLE}`](#class-table) instance with the data of the deleted row -- A [`ReducerEvent`](#class-reducerevent), which contains the data of the reducer that deleted the row. +The `Reducers` property of the context provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers). -This event should almost always be used instead of [`OnDelete`](#static-event-tableondelete). This is because often, many rows will be deleted at once, and `OnDelete` can be invoked in an arbitrary order on these rows. This means that data related to a row may already be missing when `OnDelete` is called. `OnBeforeDelete` does not have this problem. +### Record `Event` -```cs -using SpacetimeDB; -using SpacetimeDB.Types; +| Name | Description | +|-------------------------------------------------------------|--------------------------------------------------------------------------| +| [`Reducer` variant](#variant-reducer) | A reducer ran in the remote database. | +| [`SubscribeApplied` variant](#variant-subscribeapplied) | A new subscription was applied to the client cache. | +| [`UnsubscribeApplied` variant](#variant-unsubscribeapplied) | A previous subscription was removed from the client cache after a call to [`Unsubscribe`](#method-unsubscribe). | +| [`SubscribeError` variant](#variant-subscribeerror) | A previous subscription was removed from the client cache due to an error. | +| [`UnknownTransaction` variant](#variant-unknowntransaction) | A transaction ran in the remote database, but was not attributed to a known reducer. | +| [`ReducerEvent` record](#record-reducerevent) | Metadata about a reducer run. Contained in a [`Reducer` event](#variant-reducer) and [`ReducerEventContext`](#type-reducereventcontext). | +| [`Status` record](#record-status) | Completion status of a reducer run. | +| [`Reducer` record](#record-reducer) | Module-specific generated record with a variant for each reducer defined by the module. | -/* initialize, subscribe to table User... */ +#### Variant `Reducer` -User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { - Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); -}; +```csharp +record Event +{ + public record Reducer(ReducerEvent ReducerEvent) : Event; +} ``` -#### Static Event `{TABLE}.OnDelete` +Event when we are notified that a reducer ran in the remote module. The [`ReducerEvent`](#record-reducerevent) contains metadata about the reducer run, including its arguments and termination [`Status`](#record-status). -```cs -namespace SpacetimeDB.Types { +This event is passed to row callbacks resulting from modifications by the reducer. -class TABLE { - public delegate void DeleteEventHandler( - TABLE deletedValue, - SpacetimeDB.ReducerEvent dbEvent - ); - public static event DeleteEventHandler OnDelete; -} +#### Variant `SubscribeApplied` +```csharp +record Event +{ + public record SubscribeApplied : Event; } ``` -Register a delegate for when a subscribed row is being deleted from the database. If a reducer deletes many rows at once, this delegate will be invoked on those rows in arbitrary order, and data for some rows may already be missing when it is invoked. For this reason, prefer the event [`{TABLE}.OnBeforeDelete`](#static-event-tableonbeforedelete). - -The delegate takes two arguments: +Event when our subscription is applied and its rows are inserted into the client cache. -- A [`{TABLE}`](#class-table) instance with the data of the deleted row -- A [`ReducerEvent`](#class-reducerevent), which contains the data of the reducer that deleted the row. +This event is passed to [row `OnInsert` callbacks](#callback-oninsert) resulting from the new subscription. -```cs -using SpacetimeDB; -using SpacetimeDB.Types; +#### Variant `UnsubscribeApplied` -/* initialize, subscribe to table User... */ - -User.OnBeforeDelete += (User user, ReducerEvent reducerEvent) => { - Console.WriteLine($"User '{user.Name}' deleted by reducer {reducerEvent.Reducer}."); -}; +```csharp +record Event +{ + public record UnsubscribeApplied : Event; +} ``` -#### Static Event `{TABLE}.OnUpdate` +Event when our subscription is removed after a call to [`SubscriptionHandle.Unsubscribe`](#method-unsubscribe) or [`SubscriptionHandle.UnsubscribeTthen`](#method-unsubscribethen) and its matching rows are deleted from the client cache. -```cs -namespace SpacetimeDB.Types { +This event is passed to [row `OnDelete` callbacks](#callback-ondelete) resulting from the subscription ending. -class TABLE { - public delegate void UpdateEventHandler( - TABLE oldValue, - TABLE newValue, - ReducerEvent dbEvent - ); - public static event UpdateEventHandler OnUpdate; -} +#### Variant `SubscribeError` +```csharp +record Event +{ + public record SubscribeError(Exception Exception) : Event; } ``` -Register a delegate for when a subscribed row is being updated. This event is only available if the row has a column with the `#[primary_key]` attribute. +Event when a subscription ends unexpectedly due to an error. -The delegate takes three arguments: +This event is passed to [row `OnDelete` callbacks](#callback-ondelete) resulting from the subscription ending. -- A [`{TABLE}`](#class-table) instance with the old data of the updated row -- A [`{TABLE}`](#class-table) instance with the new data of the updated row -- A [`ReducerEvent`](#class-reducerevent), which contains the data of the reducer that updated the row. +#### Variant `UnknownTransaction` -```cs -using SpacetimeDB; -using SpacetimeDB.Types; - -/* initialize, subscribe to table User... */ - -User.OnUpdate += (User oldUser, User newUser, ReducerEvent reducerEvent) => { - Debug.Assert(oldUser.UserId == newUser.UserId, "Primary key never changes in an update"); - - Console.WriteLine($"User with ID {oldUser.UserId} had name changed "+ - $"from '{oldUser.Name}' to '{newUser.Name}' by reducer {reducerEvent.Reducer}."); -}; +```csharp +record Event +{ + public record UnknownTransaction : Event; +} ``` -## Observe and invoke reducers +Event when we are notified of a transaction in the remote module which we cannot associate with a known reducer. This may be an ad-hoc SQL query or a reducer for which we do not have bindings. -"Reducer" is SpacetimeDB's name for the stored procedures that run in modules inside the database. You can invoke reducers from a connected client SDK, and also receive information about which reducers are running. +This event is passed to [row callbacks](#callback-oninsert) resulting from modifications by the transaction. -`spacetime generate` generates a class [`SpacetimeDB.Types.Reducer`](#class-reducer) that contains methods and events for each reducer defined in a module. To invoke a reducer, use the method [`Reducer.{REDUCER}`](#static-method-reducerreducer) generated for it. To receive a callback each time a reducer is invoked, use the static event [`Reducer.On{REDUCER}`](#static-event-reduceronreducer). +### Record `ReducerEvent` -### Class `Reducer` +```csharp +record ReducerEvent( + Timestamp Timestamp, + Status Status, + Identity CallerIdentity, + ConnectionId? CallerConnectionId, + U128? EnergyConsumed, + R Reducer +) +``` -```cs -namespace SpacetimeDB.Types { +A `ReducerEvent` contains metadata about a reducer run. -class Reducer {} +### Record `Status` -} +```csharp +record Status : TaggedEnum<( + Unit Committed, + string Failed, + Unit OutOfEnergy +)>; ``` -This class contains a static method and event for each reducer defined in a module. + -#### Static Method `Reducer.{REDUCER}` +| Name | Description | +|-----------------------------------------------|-----------------------------------------------------| +| [`Committed` variant](#variant-committed) | The reducer ran successfully. | +| [`Failed` variant](#variant-failed) | The reducer errored. | +| [`OutOfEnergy` variant](#variant-outofenergy) | The reducer was aborted due to insufficient energy. | -```cs -namespace SpacetimeDB.Types { -class Reducer { +#### Variant `Committed` -/* void {REDUCER_NAME}(...ARGS...) */ +The reducer returned successfully and its changes were committed into the database state. An [`Event.Reducer`](#variant-reducer) passed to a row callback must have this status in its [`ReducerEvent`](#record-reducerevent). -} -} -``` +#### Variant `Failed` -For each reducer defined by a module, `spacetime generate` generates a static method which sends a request to the database to invoke that reducer. The generated function's name is the reducer's name converted to `PascalCase`. +The reducer returned an error, panicked, or threw an exception. The record payload is the stringified error message. Formatting of the error message is unstable and subject to change, so clients should use it only as a human-readable diagnostic, and in particular should not attempt to parse the message. -Reducers don't run immediately! They run as soon as the request reaches the database. Don't assume data inserted by a reducer will be available immediately after you call this method. +#### Variant `OutOfEnergy` -For reducers which accept a `ReducerContext` as their first argument, the `ReducerContext` is not included in the generated function's argument list. +The reducer was aborted due to insufficient energy balance of the module owner. -For example, if we define a reducer in Rust as follows: +### Record `Reducer` -```rust -#[spacetimedb(reducer)] -pub fn set_name( - ctx: ReducerContext, - user_id: u64, - name: String -) -> Result<(), Error>; -``` +The module bindings contains an record `Reducer` with a variant for each reducer defined by the module. Each variant has a payload containing the arguments to the reducer. -The following C# static method will be generated: +## Type `ReducerEventContext` -```cs -namespace SpacetimeDB.Types { -class Reducer { +A `ReducerEventContext` is an [`IDbContext`](#interface-idbcontext) augmented with an [`Event`](#record-reducerevent) property. `ReducerEventContext`s are passed as the first argument to [reducer callbacks](#observe-and-invoke-reducers). -public static void SendMessage(UInt64 userId, string name); +| Name | Description | +|-------------------------------------------|---------------------------------------------------------------------| +| [`Event` property](#property-event) | [`ReducerEvent`](#record-reducerevent) containing reducer metadata. | +| [`Db` property](#property-db) | Provides access to the client cache. | +| [`Reducers` property](#property-reducers) | Allows requesting reducers run on the remote database. | -} +### Property `Event` + +```csharp +class ReducerEventContext { + public readonly ReducerEvent Event; + /* other fields */ } ``` -#### Static Event `Reducer.On{REDUCER}` +The [`ReducerEvent`](#record-reducerevent) contained in the `ReducerEventContext` has metadata about the reducer which ran. -```cs -namespace SpacetimeDB.Types { -class Reducer { +### Property `Db` -public delegate void /*{REDUCER}*/Handler(ReducerEvent reducerEvent, /* {ARGS...} */); - -public static event /*{REDUCER}*/Handler On/*{REDUCER}*/Event; - -} +```csharp +class ReducerEventContext { + public RemoteTables Db; + /* other fields */ } ``` -For each reducer defined by a module, `spacetime generate` generates an event to run each time the reducer is invoked. The generated functions are named `on{REDUCER}Event`, where `{REDUCER}` is the reducer's name converted to `PascalCase`. - -The first argument to the event handler is an instance of [`SpacetimeDB.Types.ReducerEvent`](#class-reducerevent) describing the invocation -- its timestamp, arguments, and whether it succeeded or failed. The remaining arguments are the arguments passed to the reducer. Reducers cannot have return values, so no return value information is included. +The `Db` property of the context provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache). -For example, if we define a reducer in Rust as follows: +### Property `Reducers` -```rust -#[spacetimedb(reducer)] -pub fn set_name( - ctx: ReducerContext, - user_id: u64, - name: String -) -> Result<(), Error>; +```csharp +class ReducerEventContext { + public RemoteReducers Reducers; + /* other fields */ +} ``` -The following C# static method will be generated: +The `Reducers` property of the context provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers). -```cs -namespace SpacetimeDB.Types { -class Reducer { +## Type `SubscriptionEventContext` -public delegate void SetNameHandler( - ReducerEvent reducerEvent, - UInt64 userId, - string name -); -public static event SetNameHandler OnSetNameEvent; +A `SubscriptionEventContext` is an [`IDbContext`](#interface-idbcontext). Unlike the other context types, `SubscriptionEventContext` doesn't have an `Event` property. `SubscriptionEventContext`s are passed to subscription [`OnApplied`](#callback-onapplied) and [`UnsubscribeThen`](#method-unsubscribethen) callbacks. -} +| Name | Description | +|-------------------------------------------|------------------------------------------------------------| +| [`Db` property](#property-db) | Provides access to the client cache. | +| [`Reducers` property](#property-reducers) | Allows requesting reducers run on the remote database. | + +### Property `Db` + +```csharp +class SubscriptionEventContext { + public RemoteTables Db; + /* other fields */ } ``` -Which can be used as follows: +The `Db` property of the context provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache). -```cs -/* initialize, wait for onSubscriptionApplied... */ +### Property `Reducers` -Reducer.SetNameHandler += ( - ReducerEvent reducerEvent, - UInt64 userId, - string name -) => { - if (reducerEvent.Status == ClientApi.Event.Types.Status.Committed) { - Console.WriteLine($"User with id {userId} set name to {name}"); - } else if (reducerEvent.Status == ClientApi.Event.Types.Status.Failed) { - Console.WriteLine( - $"User with id {userId} failed to set name to {name}:" - + reducerEvent.ErrMessage - ); - } else if (reducerEvent.Status == ClientApi.Event.Types.Status.OutOfEnergy) { - Console.WriteLine( - $"User with id {userId} failed to set name to {name}:" - + "Invoker ran out of energy" - ); - } -}; -Reducer.SetName(USER_ID, NAME); +```csharp +class SubscriptionEventContext { + public RemoteReducers Reducers; + /* other fields */ +} ``` -### Class `ReducerEvent` +The `Reducers` property of the context provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers). -`spacetime generate` defines an class `ReducerEvent` containing an enum `ReducerType` with a variant for each reducer defined by a module. The variant's name will be the reducer's name converted to `PascalCase`. +## Type `ErrorContext` -For example, the example project shown in the Rust Module quickstart will generate the following (abridged) code. +An `ErrorContext` is an [`IDbContext`](#interface-idbcontext) augmented with an `Event` property. `ErrorContext`s are to connections' [`OnDisconnect`](#callback-ondisconnect) and [`OnConnectError`](#callback-onconnecterror) callbacks, and to subscriptions' [`OnError`](#callback-onerror) callbacks. -```cs -namespace SpacetimeDB.Types { +| Name | Description | +|-------------------------------------------|--------------------------------------------------------| +| [`Event` property](#property-event) | The error which caused the current error callback. | +| [`Db` property](#property-db) | Provides access to the client cache. | +| [`Reducers` property](#property-reducers) | Allows requesting reducers run on the remote database. | -public enum ReducerType -{ - /* A member for each reducer in the module, with names converted to PascalCase */ - None, - SendMessage, - SetName, -} -public partial class SendMessageArgsStruct -{ - /* A member for each argument of the reducer SendMessage, with names converted to PascalCase. */ - public string Text; -} -public partial class SetNameArgsStruct -{ - /* A member for each argument of the reducer SetName, with names converted to PascalCase. */ - public string Name; -} -public partial class ReducerEvent : ReducerEventBase { - // Which reducer was invoked - public ReducerType Reducer { get; } - // If event.Reducer == ReducerType.SendMessage, the arguments - // sent to the SendMessage reducer. Otherwise, accesses will - // throw a runtime error. - public SendMessageArgsStruct SendMessageArgs { get; } - // If event.Reducer == ReducerType.SetName, the arguments - // passed to the SetName reducer. Otherwise, accesses will - // throw a runtime error. - public SetNameArgsStruct SetNameArgs { get; } - /* Additional information, present on any ReducerEvent */ - // The name of the reducer. - public string ReducerName { get; } - // The timestamp of the reducer invocation inside the database. - public ulong Timestamp { get; } - // The identity of the client that invoked the reducer. - public SpacetimeDB.Identity Identity { get; } - // Whether the reducer succeeded, failed, or ran out of energy. - public ClientApi.Event.Types.Status Status { get; } - // If event.Status == Status.Failed, the error message returned from inside the module. - public string ErrMessage { get; } -} +### Property `Event` +```csharp +class SubscriptionEventContext { + public readonly Exception Event; + /* other fields */ } ``` -#### Enum `Status` - -```cs -namespace ClientApi { -public sealed partial class Event { -public static partial class Types { - -public enum Status { - Committed = 0, - Failed = 1, - OutOfEnergy = 2, -} +### Property `Db` -} -} +```csharp +class ErrorContext { + public RemoteTables Db; + /* other fields */ } ``` -An enum whose variants represent possible reducer completion statuses of a reducer invocation. +The `Db` property of the context provides access to the subscribed view of the remote database's tables. See [Access the client cache](#access-the-client-cache). -##### Variant `Status.Committed` +### Property `Reducers` -The reducer finished successfully, and its row changes were committed to the database. - -##### Variant `Status.Failed` +```csharp +class ErrorContext { + public RemoteReducers Reducers; + /* other fields */ +} +``` -The reducer failed, either by panicking or returning a `Err`. +The `Reducers` property of the context provides access to reducers exposed by the remote module. See [Observe and invoke reducers](#observe-and-invoke-reducers). -##### Variant `Status.OutOfEnergy` +## Access the client cache -The reducer was canceled because the module owner had insufficient energy to allow it to run to completion. +All [`IDbContext`](#interface-idbcontext) implementors, including [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext), have `.Db` properties, which in turn have methods for accessing tables in the client cache. -## Identity management +Each table defined by a module has an accessor method, whose name is the table name converted to `snake_case`, on this `.Db` property. The table accessor methods return table handles which inherit from [`RemoteTableHandle`](#type-remotetablehandle) and have methods for searching by index. -### Class `AuthToken` +| Name | Description | +|-------------------------------------------------------------------|---------------------------------------------------------------------------------| +| [`RemoteTableHandle`](#type-remotetablehandle) | Provides access to subscribed rows of a specific table within the client cache. | +| [Unique constraint index access](#unique-constraint-index-access) | Seek a subscribed row by the value in its unique or primary key column. | +| [BTree index access](#btree-index-access) | Seek subscribed rows by the value in its indexed column. | -The AuthToken helper class handles creating and saving SpacetimeDB identity tokens in the filesystem. +### Type `RemoteTableHandle` -#### Static Method `AuthToken.Init` +Implemented by all table handles. -```cs -namespace SpacetimeDB { +| Name | Description | +|-----------------------------------------------|------------------------------------------------------------------------------| +| [`Row` type parameter](#type-row) | The type of rows in the table. | +| [`Count` property](#property-count) | The number of subscribed rows in the table. | +| [`Iter` method](#method-iter) | Iterate over all subscribed rows in the table. | +| [`OnInsert` callback](#callback-oninsert) | Register a callback to run whenever a row is inserted into the client cache. | +| [`OnDelete` callback](#callback-ondelete) | Register a callback to run whenever a row is deleted from the client cache. | +| [`OnUpdate` callback](#callback-onupdate) | Register a callback to run whenever a subscribed row is replaced with a new version. | -class AuthToken { - public static void Init( - string configFolder = ".spacetime_csharp_sdk", - string configFile = "settings.ini", - string? configRoot = null - ); -} +#### Type `Row` +```csharp +class RemoteTableHandle +{ + /* members */ } ``` -Creates a file `$"{configRoot}/{configFolder}/{configFile}"` to store tokens. -If no arguments are passed, the default is `"%HOME%/.spacetime_csharp_sdk/settings.ini"`. +The type of rows in the table. -| Argument | Type | Meaning | -| -------------- | -------- | ---------------------------------------------------------------------------------- | -| `configFolder` | `string` | The folder to store the config file in. Default is `"spacetime_csharp_sdk"`. | -| `configFile` | `string` | The name of the config file. Default is `"settings.ini"`. | -| `configRoot` | `string` | The root folder to store the config file in. Default is the user's home directory. | +#### Property `Count` -#### Static Property `AuthToken.Token` +```csharp +class RemoteTableHandle +{ + public int Count; +} +``` -```cs -namespace SpacetimeDB { +The number of rows of this table resident in the client cache, i.e. the total number which match any subscribed query. -class AuthToken { - public static string? Token { get; } -} +#### Method `Iter` +```csharp +class RemoteTableHandle +{ + public IEnumerable Iter(); } ``` -The auth token stored on the filesystem, if one exists. - -#### Static Method `AuthToken.SaveToken` - -```cs -namespace SpacetimeDB { +An iterator over all the subscribed rows in the client cache, i.e. those which match any subscribed query. -class AuthToken { - public static void SaveToken(string token); -} +#### Callback `OnInsert` +```csharp +class RemoteTableHandle +{ + public delegate void RowEventHandler(EventContext context, Row row); + public event RowEventHandler? OnInsert; } ``` -Save a token to the filesystem. +The `OnInsert` callback runs whenever a new row is inserted into the client cache, either when applying a subscription or being notified of a transaction. The passed [`EventContext`](#type-eventcontext) contains an [`Event`](#record-event) which can identify the change which caused the insertion, and also allows the callback to interact with the connection, inspect the client cache and invoke reducers. Newly registered or canceled callbacks do not take effect until the following event. + +See [the quickstart](/docs/sdks/c-sharp/quickstart#register-callbacks) for examples of regstering and unregistering row callbacks. -### Class `Identity` +#### Callback `OnDelete` -```cs -namespace SpacetimeDB +```csharp +class RemoteTableHandle { - public struct Identity : IEquatable - { - public byte[] Bytes { get; } - public static Identity From(byte[] bytes); - public bool Equals(Identity other); - public static bool operator ==(Identity a, Identity b); - public static bool operator !=(Identity a, Identity b); - } + public delegate void RowEventHandler(EventContext context, Row row); + public event RowEventHandler? OnDelete; } ``` -A unique public identifier for a user of a database. +The `OnDelete` callback runs whenever a previously-resident row is deleted from the client cache. Newly registered or canceled callbacks do not take effect until the following event. - +See [the quickstart](/docs/sdks/c-sharp/quickstart#register-callbacks) for examples of regstering and unregistering row callbacks. -Columns of type `Identity` inside a module will be represented in the C# SDK as properties of type `byte[]`. `Identity` is essentially just a wrapper around `byte[]`, and you can use the `Bytes` property to get a `byte[]` that can be used to filter tables and so on. +#### Callback `OnUpdate` -```cs -namespace SpacetimeDB +```csharp +class RemoteTableHandle { - public struct Address : IEquatable
- { - public byte[] Bytes { get; } - public static Address? From(byte[] bytes); - public bool Equals(Address other); - public static bool operator ==(Address a, Address b); - public static bool operator !=(Address a, Address b); - } + public delegate void RowEventHandler(EventContext context, Row row); + public event RowEventHandler? OnUpdate; } ``` -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity). - -## Customizing logging +The `OnUpdate` callback runs whenever an already-resident row in the client cache is updated, i.e. replaced with a new row that has the same primary key. The table must have a primary key for callbacks to be triggered. Newly registered or canceled callbacks do not take effect until the following event. -The SpacetimeDB C# SDK performs internal logging. +See [the quickstart](/docs/sdks/c-sharp/quickstart#register-callbacks) for examples of regstering and unregistering row callbacks. -A default logger is set up automatically for you - a [`ConsoleLogger`](#class-consolelogger) for C# projects and [`UnityDebugLogger`](#class-unitydebuglogger) for Unity projects. +### Unique constraint index access -If you want to redirect SDK logs elsewhere, you can inherit from the [`ISpacetimeDBLogger`](#interface-ispacetimedblogger) and assign an instance of your class to the `SpacetimeDB.Logger.Current` static property. +For each unique constraint on a table, its table handle has a property which is a unique index handle and whose name is the unique column name. This unique index handle has a method `.Find(Column value)`. If a `Row` with `value` in the unique column is resident in the client cache, `.Find` returns it. Otherwise it returns null. -### Interface `ISpacetimeDBLogger` -```cs -namespace SpacetimeDB -{ +#### Example -public interface ISpacetimeDBLogger +Given the following module-side `User` definition: +```csharp +[Table(Name = "User", Public = true)] +public partial class User { - void Log(string message); - void LogError(string message); - void LogWarning(string message); - void LogException(Exception e); + [Unique] // Or [PrimaryKey] + public Identity Identity; + .. } +``` -} +a client would lookup a user as follows: +```csharp +User? FindUser(RemoteTables tables, Identity id) => tables.User.Identity.Find(id); ``` -This interface provides methods that are invoked when the SpacetimeDB C# SDK needs to log at various log levels. You can create custom implementations if needed to integrate with existing logging solutions. +### BTree index access -### Class `ConsoleLogger` +For each btree index defined on a remote table, its corresponding table handle has a property which is a btree index handle and whose name is the name of the index. This index handle has a method `IEnumerable Filter(Column value)` which will return `Row`s with `value` in the indexed `Column`, if there are any in the cache. -```cs -namespace SpacetimeDB { +#### Example -public class ConsoleLogger : ISpacetimeDBLogger {} +Given the following module-side `Player` definition: +```csharp +[Table(Name = "Player", Public = true)] +public partial class Player +{ + [PrimaryKey] + public Identity id; + [Index.BTree(Name = "Level")] + public uint level; + .. } ``` -An `ISpacetimeDBLogger` implementation for regular .NET applications, using `Console.Write` when logs are received. +a client would count the number of `Player`s at a certain level as follows: +```csharp +int CountPlayersAtLevel(RemoteTables tables, uint level) => tables.Player.Level.Filter(level).Count(); +``` -### Class `UnityDebugLogger` +## Observe and invoke reducers -```cs -namespace SpacetimeDB { +All [`IDbContext`](#interface-idbcontext) implementors, including [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext), have a `.Reducers` property, which in turn has methods for invoking reducers defined by the module and registering callbacks on it. -public class UnityDebugLogger : ISpacetimeDBLogger {} +Each reducer defined by the module has three methods on the `.Reducers`: -} -``` +- An invoke method, whose name is the reducer's name converted to snake case, like `set_name`. This requests that the module run the reducer. +- A callback registation method, whose name is prefixed with `on_`, like `on_set_name`. This registers a callback to run whenever we are notified that the reducer ran, including successfully committed runs and runs we requested which failed. This method returns a callback id, which can be passed to the callback remove method. +- A callback remove method, whose name is prefixed with `remove_on_`, like `remove_on_set_name`. This cancels a callback previously registered via the callback registration method. + +## Identify a client + +### Type `Identity` + +A unique public identifier for a client connected to a database. +See the [module docs](/docs/modules/c-sharp#struct-identity) for more details. + +### Type `ConnectionId` + +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity). +See the [module docs](/docs/modules/c-sharp#struct-connectionid) for more details. + +### Type `Timestamp` + +A point in time, measured in microseconds since the Unix epoch. +See the [module docs](/docs/modules/c-sharp#struct-timestamp) for more details. + +### Type `TaggedEnum` -An `ISpacetimeDBLogger` implementation for Unity, using the Unity `Debug.Log` api. +A [tagged union](https://en.wikipedia.org/wiki/Tagged_union) type. +See the [module docs](/docs/modules/c-sharp#record-taggedenum) for more details.