.NET Server SDK
Statsig's Next-gen .NET Server SDK built in our [Server Core](/server-core) framework
Setup the SDK
Install the SDK
Installation
bashdotnet add package Statsig.DotnetOr add the package reference to your
.csprojfile:xml<PackageReference Include="Statsig.Dotnet" Version="X.X.X" />Requirements
- .NET 8.0 or later
- Windows, macOS, or Linux (x64 and ARM64 supported)
Initialize the SDK
After installation, initialize the SDK using a Server Secret Key from the Statsig console.Server Secret Keys should always be kept private. If you expose one, you can disable and recreate it in the Statsig console.
An optional
optionsparameter accepts aStatsigOptionsobject to customize the SDK.csharpusing Statsig; var statsig = new Statsig.Statsig("server-secret-key"); await statsig.Initialize();You can also provide custom options:
csharpvar options = new StatsigOptionsBuilder() .SetSpecsSyncIntervalMs(10000) .SetDisableAllLogging(false) .Build(); var statsig = new Statsig("server-secret-key", options); await statsig.Initialize();For shared instance usage:
csharpvar sharedStatsig = Statsig.NewShared("server-secret-key", options); await sharedStatsig.Initialize(); var statsig = Statsig.Shared();initializeperforms a network request. Afterinitializecompletes, virtually all SDK operations are synchronous (refer to Evaluating Feature Gates in the Statsig SDK). The SDK fetches updates from Statsig in the background, independent of your API calls.
Working with the SDK
Checking a Feature Flag/Gate
After the SDK is initialized, you can fetch a Feature Gate. Feature Gates create logic branches in code that you can roll out to different users from the Statsig Console. Gates are CLOSED or OFF (equivalent toreturn false;) by default.All APIs require a user object (refer to Statsig user). To check a gate for a user:var user = new StatsigUserBuilder()
.SetUserID("user_123")
.SetEmail("user@example.com")
.Build();
var gateValue = statsig.CheckGate(user, "new_feature_gate");
if (gateValue)
{
// Gate is on, enable new feature
}
else
{
// Gate is off
}
You can also disable exposure logging for this evaluation:
var options = new EvaluationOptions(disableExposureLogging: true);
var gateValue = statsig.CheckGate(user, "new_feature_gate", options);
Reading a Dynamic Config
Feature Gates are useful for simple on/off switches with optional user targeting. To send different values (strings, numbers, etc.) to clients based on user attributes such as country, use Dynamic Configs. The API is similar to Feature Gates, but returns a JSON object from which you can retrieve typed parameters. For example:var user = new StatsigUserBuilder()
.SetUserID("user_123")
.Build();
var config = statsig.GetDynamicConfig(user, "product_config");
var productName = config.Get<string>("product_name", "Default Product");
var price = config.Get<double>("price", 9.99);
var isEnabled = config.Get<bool>("enabled", false);
var features = config.Get<List<string>>("features", new List<string>());
Console.WriteLine($"Config Name: {config.Name}");
Console.WriteLine($"Group Name: {config.GroupName}");
Console.WriteLine($"Rule ID: {config.RuleID}");
Getting a Layer/Experiment
Layers/Experiments let you run A/B/n experiments. Two APIs are available, but layers are recommended because layers make parameters reusable and support mutually exclusive experiments.var user = new StatsigUserBuilder()
.SetUserID("user_123")
.Build();
var experiment = statsig.GetExperiment(user, "button_color_test");
var buttonColor = experiment.Get<string>("color", "blue");
var fontSize = experiment.Get<int>("font_size", 14);
var showBorder = experiment.Get<bool>("show_border", true);
Console.WriteLine($"Experiment Name: {experiment.Name}");
Console.WriteLine($"Group Name: {experiment.GroupName}");
Console.WriteLine($"Rule ID: {experiment.RuleID}");
Console.WriteLine($"Button Color: {buttonColor}");
var user = new StatsigUserBuilder()
.SetUserID("user_123")
.Build();
var layer = statsig.GetLayer(user, "user_prefs_layer");
var theme = layer.Get<string>("theme", "light");
var language = layer.Get<string>("language", "en");
var notifications = layer.Get<bool>("notifications_enabled", true);
Console.WriteLine($"Layer Name: {layer.Name}");
Console.WriteLine($"Allocated Experiment: {layer.AllocatedExperimentName}");
Console.WriteLine($"Group Name: {layer.GroupName}");
Console.WriteLine($"Rule ID: {layer.RuleID}");
The SDK automatically logs layer parameter access as exposure events unless you disable this behavior with EvaluationOptions.
Logging an Event
After setting up a Feature Gate or Experiment, track custom events to measure how features or experiment groups affect user behavior. Call the Log Event API and specify the user and event name. You can also provide a value and metadata:
var user = new StatsigUserBuilder()
.SetUserID("user_123")
.Build();
statsig.LogEvent(user, "button_clicked");
statsig.LogEvent(user, "purchase_completed", 29.99);
statsig.LogEvent(user, "page_view", "homepage", new Dictionary<string, string>
{
["referrer"] = "google",
["campaign"] = "summer_sale"
});
statsig.LogEvent(user, "video_watched", 120, new Dictionary<string, string>
{
["video_id"] = "abc123",
["quality"] = "1080p"
});
The LogEvent method supports multiple overloads:
LogEvent(user, eventName)LogEvent(user, eventName, stringValue, metadata)LogEvent(user, eventName, intValue, metadata)LogEvent(user, eventName, doubleValue, metadata)
Retrieving Feature Gate Metadata
To retrieve more information about a gate evaluation than a boolean value, use the Get Feature Gate API, which returns a FeatureGate object:
var user = new StatsigUserBuilder()
.SetUserID("user_123")
.Build();
var gate = statsig.GetFeatureGate(user, "new_feature_gate");
Console.WriteLine($"Gate Name: {gate.Name}");
Console.WriteLine($"Gate Value: {gate.Value}");
Console.WriteLine($"Rule ID: {gate.RuleID}");
Console.WriteLine($"ID Type: {gate.IDType}");
if (gate.EvaluationDetails != null)
{
Console.WriteLine($"Config Sync Time: {gate.EvaluationDetails.ConfigSyncTime}");
Console.WriteLine($"Init Time: {gate.EvaluationDetails.InitTime}");
Console.WriteLine($"Reason: {gate.EvaluationDetails.Reason}");
}
Parameter Stores
If you want dynamic control over whether a value comes from a Feature Gate, Experiment, or Dynamic Config outside your deployment cycle, use Parameter Stores. A Parameter Store lets you define a parameter that you can change at any point in the Statsig console. Parameter Stores are optional, but parameterizing your application supports future flexibility and allows non-technical Statsig users to turn parameters into experiments.
# Get a Parameter Store by name
param_store = statsig.getParameterStore(user, "my_parameter_store")
Retrieving Parameter Values
Parameter Store provides methods for retrieving values of different types with fallback defaults.
Evaluation options
You can disable exposure logging when retrieving a parameter store:
var store = statsig.GetParameterStore(user, name!, options);
if (store == null)
{
throw new Exception($"Parameter store {name} not found");
}
var x = store.GetBool(paramName, default(bool));
var y = store.GetString(paramName, "");
var z = store.GetDictionary(paramName, new Dictionary<string, object>());
Using shared instance
To access a single Statsig instance globally throughout your codebase, use the shared instance singleton pattern:
var sharedStatsig = Statsig.NewShared("server-secret-key");
await sharedStatsig.Initialize();
// Later, anywhere in your codebase
var statsig = Statsig.Shared();
// Use the shared instance
var result = statsig.CheckGate(user, "my_gate");
The shared instance is useful for:
- Singleton pattern usage across your application
- Dependency injection scenarios
- Avoiding multiple SDK instances
Clean up the shared instance on shutdown:
var statsig = Statsig.Shared();
await statsig.FlushEvents();
await statsig.Shutdown();
Statsig.RemoveSharedInstance();
Manual exposures
By default, the SDK automatically logs an exposure event when you check a gate, get a config, get an experiment, or call get() on a layer parameter. To delay exposure logging until the user actually uses the feature, use manual exposures.
All main SDK functions (CheckGate, GetDynamicConfig, GetExperiment, GetLayer) accept an optional EvaluationOptions parameter. When disableExposureLogging is set to true, the SDK doesn't automatically log an exposure event. You can then manually log the exposure using the corresponding manual exposure logging method:
var result = statsig.CheckGate(user, "a_gate_name", new EvaluationOptions(disableExposureLogging: true));
statsig.ManuallyLogGateExposure(user, "a_gate_name");
Statsig User
The StatsigUser object represents a user in Statsig. You must provide a userID or at least one of the customIDs to identify the user.
In addition to userID, the top-level fields on StatsigUser are: email, ip, userAgent, country, locale, and appVersion. You can also pass key-value pairs in the custom field for targeting.
Private attributes
Private attributes are user attributes used for evaluation but not forwarded to any integrations. Use them for PII or sensitive data that you don't want to send to third-party services.
StatsigUser represents the user context for feature flag evaluation. Use StatsigUserBuilder to create user instances:
var user = new StatsigUserBuilder()
.SetUserID("user_123")
.SetEmail("user@example.com")
.SetIP("192.168.1.1")
.SetUserAgent("Mozilla/5.0...")
.SetCountry("US")
.SetLocale("en-US")
.SetAppVersion("1.2.3")
.SetCustomIDs(new Dictionary<string, string>
{
["employee_id"] = "emp_456",
["team_id"] = "team_789"
})
.AddCustomID("department_id", "dept_123")
.SetCustomProperties(new Dictionary<string, object>
{
["subscription_tier"] = "premium",
["account_age_days"] = 365,
["is_beta_user"] = true
})
.AddCustomProperty("last_login", DateTime.UtcNow)
.SetPrivateAttributes(new Dictionary<string, object>
{
["internal_user_score"] = 0.85,
["risk_level"] = "low"
})
.AddPrivateAttribute("pii_hash", "abc123def456")
.Build();
Builder methods
Dispose of StatsigUser instances when done:
using var user = new StatsigUserBuilder()
.SetUserID("user_123")
.Build();
Statsig Options
You can pass an optional options parameter in addition to sdkKey during initialization to customize the Statsig client.
Proxy and custom network routing
The.NET Server Core SDK uses SetProxyConfig(ProxyConfig) for a standard outbound HTTP proxy. If you are routing config sync through Statsig Forward Proxy or another custom spec source, use SetSpecAdapterConfig(SpecAdapterConfig). If you only need custom Statsig endpoints, use SetSpecsURL, SetLogEventURL, and SetIDListsURL.var proxyConfig = new ProxyConfig
{
ProxyHost = "proxy.example.com",
ProxyPort = 8080,
ProxyAuth = "username:password", // Optional
ProxyProtocol = "http", // Optional: "http" or "https"
CaCertPath = "/etc/ssl/certs/corporate-ca.pem" // Optional
};
var options = new StatsigOptionsBuilder()
.SetProxyConfig(proxyConfig)
.Build();
var statsig = new Statsig("server-secret-key", options);
ProxyConfig properties
- ProxyHost (string): The hostname or IP address of the proxy server
- ProxyPort (int): The port number of the proxy server
- ProxyAuth (string, optional): Authentication credentials in the format "username:password"
- ProxyProtocol (string, optional): The protocol to use for the proxy connection ("http" or "https")
- CaCertPath (string, optional): Path to a PEM CA bundle for outbound TLS
Statsig Forward Proxy example
var specAdapterConfig = new SpecAdapterConfig(
adapterType: "network_grpc_websocket",
specsUrl: "http://forward-proxy.internal:50051"
);
var options = new StatsigOptionsBuilder()
.SetSpecAdapterConfig(specAdapterConfig)
.SetFallbackToStatsigApi(true)
.Build();
Shutting Statsig Down
Because events are batched and periodically flushed, some events may not have been sent when your app or server shuts down. To ensure all logged events are flushed, call shutdown() before shutting down your app or server.
await statsig.FlushEvents();
await statsig.Shutdown();
statsig.Dispose();
For shared instances:
var statsig = Statsig.Shared();
await statsig.FlushEvents();
await statsig.Shutdown();
Statsig.RemoveSharedInstance();
Methods
- FlushEvents(): Immediately flush any pending events to Statsig servers
- Shutdown(): Gracefully shutdown the SDK, flushing events and cleaning up resources
- Dispose(): Release native resources (implements IDisposable)
Call FlushEvents() before Shutdown() to ensure all events are sent. Always call Dispose() or use using statements to clean up resources.
Client SDK Bootstrapping | SSR
If you use the Statsig client SDK in a browser or mobile app, you can bootstrap the client SDK with values from the server SDK to avoid a network request on the client. This is useful for server-side rendering (SSR) or to reduce the number of network requests on the client.
var user = new StatsigUserBuilder()
.SetUserID("user_123")
.Build();
var initResponse = statsig.GetClientInitializeResponse(user);
var options = new ClientInitResponseOptions
{
HashAlgorithm = "sha256",
ClientSDKKey = "client-sdk-key",
IncludeLocalOverrides = false
};
var customInitResponse = statsig.GetClientInitializeResponse(user, options);
The GetClientInitializeResponse method returns a JSON string containing the initialization data needed by client-side SDKs. This enables server-side rendering and reduces client initialization time.
ClientInitResponseOptions
- HashAlgorithm: Hash algorithm for response integrity (default: "djb2")
- ClientSDKKey: Client SDK key to include in response
- IncludeLocalOverrides: Whether to include local overrides in the response (default: false)
Working with IP or UserAgent values
The server SDK doesn't automatically use ip or userAgent for gate evaluation because it doesn't have access to request headers. To use derived attributes such as Browser Name/Version, OS Name/Version, and Country, manually set the ip and userAgent fields on the user object when calling GetClientInitializeResponse.
Working with IDs
To ensure accurate config evaluation, the server SDK needs access to all user attributes that the client SDK uses. Pass all of these attributes to the server SDK, using cookies if needed to ensure they are attached on first requests. If the user objects on the client and server aren't identical, modern SDKs throw an InvalidBootstrap warning.
StableID. Manage the lifecycle of this ID to keep it consistent between client and server. Managing this with a cookie is often the simplest approach; refer to Keeping StableID Consistent. If StableID differs between client and server, a BootstrapStableIDMismatch warning appears, and checks with that warning don't contribute to experiment analyses.getClientInitializeResponse and the legacy JS SDK
If you are migrating from the legacy JS Client, you will need to make some updates to how your server SDK generates values. The default hashing algorithm was changed from sha256 to djb2 for performance and size reasons.
Local overrides
Local Overrides let you override the values of gates, configs, experiments, and layers for testing purposes without changing the configuration in the Statsig console.
statsig.OverrideGate("test_gate", true);
statsig.OverrideDynamicConfig("test_config", new Dictionary<string, object>
{
["color"] = "red",
["size"] = 42,
["enabled"] = true
});
statsig.OverrideExperiment("test_experiment", new Dictionary<string, object>
{
["variant"] = "treatment",
["multiplier"] = 1.5
});
statsig.OverrideExperimentByGroupName("test_experiment", "treatment_group");
statsig.OverrideLayer("test_layer", new Dictionary<string, object>
{
["theme"] = "dark",
["font_size"] = 16
});
statsig.OverrideParameterStore("testing123", new Dictionary<string, object>()
{
["brush_color"] = "blue",
["monochromatic"] = true,
["weight"] = 42,
["gradient"] = 3.14,
["pen_sizes"] = [1, 2, 3, 4, 5],
["artwork"] = new Dictionary<string, object>()
{
["nesting"] = "treatment"
}
});
You can also specify a user ID for targeted overrides:
statsig.OverrideGate("test_gate", true, "user_123");
statsig.OverrideDynamicConfig("test_config", new Dictionary<string, object>
{
["special_feature"] = true
}, "user_123");
Local overrides are useful for:
- Testing specific configurations during development
- QA testing with known values
- Debugging feature flag behavior
- Integration testing with predictable results
Overrides persist for the lifetime of the Statsig instance and affect all evaluations unless you provide a specific user ID.
Persistent storage
The Persistent Storage interface lets you implement custom storage for user-specific configurations. Use it to persist user assignments across sessions, ensuring consistent experiment groups when users return. This is useful for client-side A/B testing where users must always see the same variant.
using System.Collections.Generic;
using Statsig;
// Implement PersistentStorage to control how user stickiness is stored.
public class MyPersistentStorage : PersistentStorage
{
private readonly Dictionary<string, Dictionary<string, StickyValues>> _store = new();
public override IDictionary<string, StickyValues> Load(string key)
{
// Load persisted values for this user from your backing store.
return _store.TryGetValue(key, out var configs)
? new Dictionary<string, StickyValues>(configs)
: new Dictionary<string, StickyValues>();
}
public override void Save(string key, string configName, StickyValues data)
{
// Persist the sticky assignment (database, Redis, etc.).
if (!_store.TryGetValue(key, out var configs))
{
configs = new Dictionary<string, StickyValues>();
_store[key] = configs;
}
configs[configName] = data;
}
public override void Delete(string key, string configName)
{
// Remove the persisted value for this config/user.
if (_store.TryGetValue(key, out var configs))
{
configs.Remove(configName);
}
}
}
Data store
The Data Store interface lets you implement custom storage for Statsig configurations, enabling advanced caching strategies and integration with your preferred storage systems such as Redis.
using System.Threading.Tasks;
using Statsig;
public class MyDataStore : DataStore
{
public Task Initialize()
{
// Perform any initialization needed for your data store.
return Task.CompletedTask;
}
public Task Shutdown()
{
// Clean up resources.
return Task.CompletedTask;
}
public DataStoreResponse Get(string key)
{
// Retrieve data for the given key.
// This is called during SDK evaluation.
return Task.FromResult<DataStoreResponse?>(null);
}
public void Set(string key, string value, long? time = null)
{
// Store data for the given key.
// Called when SDK receives updates from Statsig.
return Task.CompletedTask;
}
public bool SupportsPollingUpdatesFor(string key)
{
// Return true if your store supports polling updates for this key, false otherwise.
return Task.FromResult(false).CompletedTask;
}
}
// Use data store
var options = new StatsigOptionsBuilder()
.SetDataStore(new MyDataStore())
.Build();
var statsig = new Statsig("server-secret-key", options);
await statsig.Initialize();
Performance benefits
The .NET Core SDK uses Statsig's high-performance Rust evaluation engine through FFI bindings:
- Native Rust evaluation engine handles all rule processing
- .NET wrapper provides familiar C# APIs and type safety
- Automatic memory management between .NET and Rust boundaries
- Thread-safe operations across the FFI boundary
Async/await support
All network operations are fully async:
await statsig.Initialize();
await statsig.FlushEvents();
await statsig.Shutdown();
Evaluation methods are synchronous for optimal performance:
var result = statsig.CheckGate(user, "gate_name");
Thread safety
The Statsig instance is thread-safe and can be used concurrently across multiple threads. Use the shared instance singleton pattern for application-wide usage:
var sharedStatsig = Statsig.NewShared("server-secret-key");
await sharedStatsig.Initialize();
var statsig = Statsig.Shared();
Was this helpful?