C# SDK
Rudder.Sdk is the .NET client SDK for Rudder (namespace RudderSdk,
generated models under RudderSdk.Models.*). It targets netstandard2.1, so
it runs on .NET, Xamarin and Unity, uses Task/async, and depends only on
Newtonsoft.Json. For a Unity game use the Unity SDK,
which wraps this package with Unity adapters.
The SDK covers only the kernel: authentication, the player profile (player, identities, wallets, counters), the currency catalog, player storage and module calls. Game features are modules, called through a generated module client. There are no resource rows and no project storage on the client.
Current version: 0.1.0. Versions stay 0.x until the API is stable, and
any minor release may break it.
Installation
Section titled “Installation”The package is published to the Rudder NuGet registry:
dotnet nuget add source https://hub.rudder.build/api/packages/rudder/nuget/index.json --name rudderdotnet add package Rudder.Sdk --version 0.1.0Client setup
Section titled “Client setup”Create one RudderClient per application lifetime and keep it.
using RudderSdk;
var client = new RudderClient(new RudderClientOptions{ BaseUrl = "https://api.rudder.build", ProjectKey = "your-sdk-key", // per-environment SDK key from https://app.rudder.build/});
await client.Auth.LoginWithDeviceAsync(region: "global", language: "en");var profile = await client.Player.LoadAsync();
using var cts = new CancellationTokenSource();_ = client.Sync.RunAsync(cts.Token);Only BaseUrl and ProjectKey are required; the constructor throws
ArgumentException when either is missing.
Options and pluggable components
Section titled “Options and pluggable components”The interfaces live in RudderSdk.Abstractions. The defaults work, but two of
them are not enough for a shipped game:
| Option | Type | Default | Notes |
|---|---|---|---|
Transport | IRudderTransport | HttpClientTransport (10 s timeout) | Pass your own HTTP stack, or new HttpClientTransport(baseUrl, httpClient). Any transport is wrapped by the SDK retry layer. |
TokenStore | ITokenStore | InMemoryTokenStore | Tokens are lost on restart. Provide a durable store. |
DeviceIdProvider | IDeviceIdProvider | GuidDeviceIdProvider | A new GUID per instance, so every restart is a new player. Persist the id yourself. |
EventDispatcher | Action<Action> | runs inline | Where state Changed handlers run. |
Logger | IRudderLogger | none | Diagnostic sink. |
Platform | string | detected (ios, android, web, else other) | Sent on every login and token refresh; used by segment rules. |
AppVersion | string | not sent | Your build version, sent on every login and token refresh. |
A durable token store:
using RudderSdk.Abstractions;
public sealed class FileTokenStore : ITokenStore{ public string? GetAccessToken() => /* read from disk */; public string? GetRefreshToken() => /* read from disk */; public void SaveTokens(string accessToken, string refreshToken) => /* write both */; public void Clear() => /* delete the file */;}Authentication
Section titled “Authentication”Sign in with a device id, with Google or Apple (pass the ID token from the provider’s sign-in flow), or through your own backend with the project’s custom auth webhook. Providers are configured per environment under Project Settings → Authentication.
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Rook");
await client.Auth.LoginWithGoogleAsync(idToken, "global", "en");await client.Auth.LoginWithAppleAsync(idToken, "global", "en");
await client.Auth.LoginWithCustomAsync(new JObject { ["ticket"] = ticket }, "global", "en");await client.Auth.LoginWithCustomAsync(new JObject { ["ticket"] = ticket }, "global", "en", provider: "steam");Linking identities
Section titled “Linking identities”A signed-in player can attach more identities to the same account. A conflict
(the identity belongs to another player, or the provider is already linked)
throws RudderApiException with status 409.
await client.Auth.LinkWithGoogleAsync(idToken);await client.Auth.LinkWithAppleAsync(idToken);await client.Auth.LinkWithCustomAsync(new JObject { ["ticket"] = ticket }, provider: "steam");
// The last remaining identity cannot be unlinked (400).await client.Auth.UnlinkIdentityAsync("google"); // "device" | "google" | "apple" | "custom" | "custom:<name>"Session lifecycle
Section titled “Session lifecycle”- Tokens from login are saved in your
ITokenStoreand sent as a bearer token. - A 401 triggers one shared token refresh and one transparent retry. Do not add your own retry.
- If the refresh fails, tokens are cleared and
Auth.AuthStateChangedfiresRudderAuthState.SignedOut. A login firesSignedIn. Auth.Logout()drops the session.Auth.RefreshAsync()forces a refresh and returnsfalsewhen the session could not be renewed.- Every login and logout drops the cached state values.
client.Auth.AuthStateChanged += state =>{ if (state == RudderAuthState.SignedOut) ShowLoginScreen();};API surface
Section titled “API surface”| Property | Type | Main members |
|---|---|---|
Auth | AuthService | LoginWithDevice/Google/Apple/CustomAsync, LinkWithGoogle/Apple/CustomAsync, UnlinkIdentityAsync, RefreshAsync, Logout, AuthStateChanged |
Player | SyncedState<PlayerProfile> | Value (Player, Identities, Wallets, Counters), LoadAsync, ReloadAsync, Changed |
Catalog | SyncedState<IReadOnlyDictionary<string, CatalogCurrency>> | currencies by slug |
Storage | StorageState | Value (all items), SaveAsync(type, data), DeleteAsync(type) |
Modules | ModulesService | CallAsync<T>(module, fn, args[, moduleVersion]) |
Sync | SyncEngine | RunAsync(ct), TickAsync(utcNow), PollAsync(), Paused |
Every async method takes an optional CancellationToken as its last
parameter.
State and sync
Section titled “State and sync”Each state object caches its value. LoadAsync fetches once, ReloadAsync
always fetches, IsLoaded tells whether a value is there, and every fetch
raises Changed.
Sync polls GET /sdk/v1/sync every 30 seconds (±20% jitter) and reloads
the loaded states whose revision grew (profile, catalog, storage). It
never runs on its own: start Sync.RunAsync(ct) on a background task or call
Sync.TickAsync(DateTime.UtcNow) from your game loop. Set Sync.Paused
while the app is in the background.
Player and counters
Section titled “Player and counters”var profile = await client.Player.LoadAsync();var gold = profile.Wallets?.FirstOrDefault(w => w.Currency == "gold")?.Balance ?? 0;var wins = profile.Counters != null && profile.Counters.TryGetValue("wins", out var w) ? w : 0;
client.Player.Changed += p => RenderWallets(p.Wallets);Counters holds the values of number counters
that are visible to game clients. There is no client-side grant or spend call;
call Player.ReloadAsync() after a module call that changes wallets or
counters.
Catalog
Section titled “Catalog”var currencies = await client.Catalog.LoadAsync();if (currencies.TryGetValue("gold", out var gold)) Console.WriteLine(gold.Name);Storage
Section titled “Storage”One item per player and type; Data is a string, so serialize JSON
yourself.
await client.Storage.SaveAsync("settings", JsonConvert.SerializeObject(settings));var items = await client.Storage.LoadAsync();var raw = items.FirstOrDefault(i => i.Type == "settings")?.Data;await client.Storage.DeleteAsync("settings");The state holds all items, loaded 100 per request. See Storage for limits.
Module calls
Section titled “Module calls”Generate a typed client and commit the file:
RUDDER_URL=https://api.rudder.build RUDDER_TOKEN=... RUDDER_PROJECT=... \ rudder client generate --lang csharp --out RudderModules.g.csusing RudderSdk.Modules;
var m = new RudderModules(client);await m.Leaderboards.SubmitAsync(new LeaderboardsSubmitArgs { Slug = "weekly", Score = 120 });var top = await m.Leaderboards.TopAsync(new LeaderboardsTopArgs { Slug = "weekly", Limit = 10 });Generated methods call
client.Modules.CallAsync<T>(module, fn, args, moduleVersion, ct). You can
call it directly; use JToken for an untyped result:
var result = await client.Modules.CallAsync<JToken>("remote_config", "get", new { });- Null
argsare sent as{}. - Each call carries an
Idempotency-Key(a new GUID per call). The backend returns the stored response for a repeated key. - Network errors, 5xx and 409
conflictare retried up to 2 times with jittered backoff and the same key. Other 4xx are never retried. - Generated clients send
X-Rudder-Module-Version: <module>@<version>.
See Generated module clients for error
constants and remote_config helpers.
Error handling
Section titled “Error handling”Failures are exceptions in the RudderSdk namespace:
| Exception | When |
|---|---|
RudderModuleException | HTTP 422, a module business error; Code is the module’s code |
RudderAuthException | HTTP 401 after the automatic refresh failed |
RudderNotFoundException | HTTP 404 |
RudderRateLimitException | HTTP 429 |
RudderNetworkException | No response (connectivity loss or timeout); StatusCode is 0 |
RudderApiException | Base class; any other non-success status |
All of them carry StatusCode, Code and RequestId (quote it in support
tickets). Kernel codes are in the generated RudderSdk.Models.RudderErrorCodes;
module codes are in the generated <Module><Fn>Errors classes. Custom
transports map failures with RudderApiException.FromResponse.
try{ await m.Store.BuyAsync(new StoreBuyArgs { OfferSlug = "starter_pack" }); await client.Player.ReloadAsync();}catch (RudderModuleException ex) when (ex.Code == StoreBuyErrors.InsufficientFunds){ ShowNotEnoughFunds();}catch (RudderNetworkException){ ShowOfflineBanner();}A module crash is a 500 and a timeout a 504, both RudderApiException
without details.
Unity vs. plain C#
Section titled “Unity vs. plain C#”The Unity package build.rudder.sdk ships Rudder.Sdk.dll and adds:
- an HTTP transport on
UnityWebRequest; - a
PlayerPrefstoken store and a persistent device id; - the
Ruddercomponent, which builds the client from aRudderConfigurationasset, pumpsSyncevery frame, pauses it in the background and raisesChangedon the main thread.
The client surface is the same. See the Unity SDK page.