Skip to content

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.

The package is published to the Rudder NuGet registry:

Terminal window
dotnet nuget add source https://hub.rudder.build/api/packages/rudder/nuget/index.json --name rudder
dotnet add package Rudder.Sdk --version 0.1.0

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.

The interfaces live in RudderSdk.Abstractions. The defaults work, but two of them are not enough for a shipped game:

OptionTypeDefaultNotes
TransportIRudderTransportHttpClientTransport (10 s timeout)Pass your own HTTP stack, or new HttpClientTransport(baseUrl, httpClient). Any transport is wrapped by the SDK retry layer.
TokenStoreITokenStoreInMemoryTokenStoreTokens are lost on restart. Provide a durable store.
DeviceIdProviderIDeviceIdProviderGuidDeviceIdProviderA new GUID per instance, so every restart is a new player. Persist the id yourself.
EventDispatcherAction<Action>runs inlineWhere state Changed handlers run.
LoggerIRudderLoggernoneDiagnostic sink.
Platformstringdetected (ios, android, web, else other)Sent on every login and token refresh; used by segment rules.
AppVersionstringnot sentYour 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 */;
}

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");

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>"
  • Tokens from login are saved in your ITokenStore and 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.AuthStateChanged fires RudderAuthState.SignedOut. A login fires SignedIn.
  • Auth.Logout() drops the session. Auth.RefreshAsync() forces a refresh and returns false when the session could not be renewed.
  • Every login and logout drops the cached state values.
client.Auth.AuthStateChanged += state =>
{
if (state == RudderAuthState.SignedOut) ShowLoginScreen();
};
PropertyTypeMain members
AuthAuthServiceLoginWithDevice/Google/Apple/CustomAsync, LinkWithGoogle/Apple/CustomAsync, UnlinkIdentityAsync, RefreshAsync, Logout, AuthStateChanged
PlayerSyncedState<PlayerProfile>Value (Player, Identities, Wallets, Counters), LoadAsync, ReloadAsync, Changed
CatalogSyncedState<IReadOnlyDictionary<string, CatalogCurrency>>currencies by slug
StorageStorageStateValue (all items), SaveAsync(type, data), DeleteAsync(type)
ModulesModulesServiceCallAsync<T>(module, fn, args[, moduleVersion])
SyncSyncEngineRunAsync(ct), TickAsync(utcNow), PollAsync(), Paused

Every async method takes an optional CancellationToken as its last parameter.

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.

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.

var currencies = await client.Catalog.LoadAsync();
if (currencies.TryGetValue("gold", out var gold)) Console.WriteLine(gold.Name);

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.

Generate a typed client and commit the file:

Terminal window
RUDDER_URL=https://api.rudder.build RUDDER_TOKEN=... RUDDER_PROJECT=... \
rudder client generate --lang csharp --out RudderModules.g.cs
using 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 args are 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 conflict are 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.

Failures are exceptions in the RudderSdk namespace:

ExceptionWhen
RudderModuleExceptionHTTP 422, a module business error; Code is the module’s code
RudderAuthExceptionHTTP 401 after the automatic refresh failed
RudderNotFoundExceptionHTTP 404
RudderRateLimitExceptionHTTP 429
RudderNetworkExceptionNo response (connectivity loss or timeout); StatusCode is 0
RudderApiExceptionBase 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.

The Unity package build.rudder.sdk ships Rudder.Sdk.dll and adds:

  • an HTTP transport on UnityWebRequest;
  • a PlayerPrefs token store and a persistent device id;
  • the Rudder component, which builds the client from a RudderConfiguration asset, pumps Sync every frame, pauses it in the background and raises Changed on the main thread.

The client surface is the same. See the Unity SDK page.