
## Setup the SDK

{% steps %}
{% step title="Install the SDK" %}
## Installation

```bash
dotnet add package Statsig.Dotnet
```

Or add the package reference to your `.csproj` file:

```xml
<PackageReference Include="Statsig.Dotnet" Version="X.X.X" />
```

### Requirements

* **.NET 8.0** or later
* **Windows, macOS, or Linux** (x64 and ARM64 supported)
{% /step %}

{% step title="Initialize the SDK" %}
After installation, initialize the SDK using a [Server Secret Key from the Statsig console](https://console.statsig.com/api_keys).

{% callout type="warning" %}
Server Secret Keys should always be kept private. If you expose one, you can disable and recreate it in the Statsig console.
{% /callout %}

An optional `options` parameter accepts a `StatsigOptions` object to customize the SDK.

```csharp
using Statsig;

var statsig = new Statsig.Statsig("server-secret-key");
await statsig.Initialize();
```

You can also provide custom options:

```csharp
var options = new StatsigOptionsBuilder()
    .SetSpecsSyncIntervalMs(10000)
    .SetDisableAllLogging(false)
    .Build();

var statsig = new Statsig("server-secret-key", options);
await statsig.Initialize();
```

For shared instance usage:

```csharp
var sharedStatsig = Statsig.NewShared("server-secret-key", options);
await sharedStatsig.Initialize();

var statsig = Statsig.Shared();
```

`initialize` performs a network request. After `initialize` completes, virtually all SDK operations are synchronous (refer to [Evaluating Feature Gates in the Statsig SDK](https://blog.statsig.com/evaluating-feature-gates-in-the-statsig-sdk-a6f8881a1ad8)). The SDK fetches updates from Statsig in the background, independent of your API calls.
{% /step %}
{% /steps %}

## Working with the SDK

### Checking a Feature Flag/Gate

After the SDK is initialized, you can fetch a [**Feature Gate**](/feature-flags/overview). 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 to `return false;`) by default.

All APIs require a user object (refer to [Statsig user](#statsig-user)). To check a gate for a user:

```csharp
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:

```csharp
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**](/dynamic-config/overview). The API is similar to Feature Gates, but returns a JSON object from which you can retrieve typed parameters. For example:

```csharp
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](/experiments/layers-overview) are recommended because layers make parameters reusable and support mutually exclusive experiments.

```csharp
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}");
```

```csharp
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:

```csharp
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:

```csharp
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.

```csharp
# 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.

{% accordion title="Param Store Methods" %}
* **GetBool(string, default(bool))**: Pulls key value of type boolean
* **GetString(string, "")**: Pulls key value of type string
* **GetLong(string, default(long))**: Pulls key value of type long
* **GetDouble(string, default(double))**: Pulls key value of type double
* **GetList(string, new List\<>())**: Pulls key value of type list
* **GetDictionary(string, new Dictionary\<string, object>())**: Pulls key value of type Dictionary
{% /accordion %}

### Evaluation options

You can disable exposure logging when retrieving a parameter store:

```csharp
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:

```csharp
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:

```csharp
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:

{% tabs %}
{% tab title="Feature Gates" %}
```csharp
var result = statsig.CheckGate(user, "a_gate_name", new EvaluationOptions(disableExposureLogging: true));
```

```csharp
statsig.ManuallyLogGateExposure(user, "a_gate_name");
```
{% /tab %}

{% tab title="Dynamic Configs" %}
```csharp
var config = statsig.GetDynamicConfig(user, "a_dynamic_config_name", new EvaluationOptions(disableExposureLogging: true));
```

```csharp
statsig.ManuallyLogDynamicConfigExposure(user, "a_dynamic_config_name");
```
{% /tab %}

{% tab title="Experiments" %}
```csharp
var experiment = statsig.GetExperiment(user, "an_experiment_name", new EvaluationOptions(disableExposureLogging: true));
```

```csharp
statsig.ManuallyLogExperimentExposure(user, "an_experiment_name");
```
{% /tab %}

{% tab title="Layers" %}
```csharp
var layer = statsig.GetLayer(user, "a_layer_name", new EvaluationOptions(disableExposureLogging: true));
var paramValue = layer.Get("a_param_name", "fallback_value");
```

```csharp
statsig.ManuallyLogLayerParameterExposure(user, "a_layer_name", "a_param_name");
```
{% /tab %}
{% /tabs %}

## 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.

When calling APIs that require a user, pass as much information as possible to enable advanced gate and config conditions (such as country or OS/browser checks) and to measure experiment impact accurately. As explained in [why an ID is always required for server SDKs](/sdks/user#why-is-an-id-always-required-for-server-sdks), at least one identifier (userID or customID) is required to provide a consistent experience for each 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:

```csharp
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

{% accordion title="Builder Methods" %}
* **SetUserID(string)**: Set the primary user ID
* **SetEmail(string)**: Set user email
* **SetIP(string)**: Set user IP address
* **SetUserAgent(string)**: Set browser user agent
* **SetCountry(string)**: Set user country
* **SetLocale(string)**: Set user locale
* **SetAppVersion(string)**: Set app version
* **SetCustomIDs(Dictionary\<string, string>)**: Set all custom IDs
* **AddCustomID(string, string)**: Add a single custom ID
* **SetCustomProperties(Dictionary\<string, object>)**: Set all custom properties
* **AddCustomProperty(string, object)**: Add a single custom property
* **SetPrivateAttributes(Dictionary\<string, object>)**: Set all private attributes
* **AddPrivateAttribute(string, object)**: Add a single private attribute
* **Build()**: Create the StatsigUser instance
{% /accordion %}

Dispose of StatsigUser instances when done:

```csharp
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.

{% accordion title="StatsigOptions" %}
`StatsigOptions` can be configured using the `StatsigOptionsBuilder` pattern:

```csharp
var options = new StatsigOptionsBuilder()
    .SetSpecsURL("https://custom-api.statsig.com/v1/download_config_specs")
    .SetLogEventURL("https://custom-api.statsig.com/v1/rgstr")
    .SetEnvironment("production")
    .SetSpecsSyncIntervalMs(30000)
    .SetEventLoggingMaxQueueSize(1000)
    .SetWaitForCountryLookupInit(true)
    .SetWaitForUserAgentInit(true)
    .SetDisableCountryLookup(false)
    .SetDisableUserAgentParsing(false)
    .SetDisableAllLogging(false)
    .SetInitTimeoutMs(3000)
    .SetFallbackToStatsigApi(false)
    .SetEnableIDLists(true)
    .SetIDListsURL("https://custom-api.statsig.com/v1/get_id_lists")
    .SetIDListsSyncIntervalMs(60000)
    .SetGlobalCustomFields(new Dictionary<string, object>
    {
        ["app_version"] = "1.2.3",
        ["build_number"] = "456"
    })
    .Build();

var statsig = new Statsig("server-secret-key", options);
```

## Available options

* **SetSpecsURL(string)**: Override the default specs download endpoint
* **SetLogEventURL(string)**: Override the default event logging endpoint
* **SetEnvironment(string)**: Set the environment tier (e.g., "production", "staging")
* **SetSpecsSyncIntervalMs(int)**: How often to sync configuration specs (default: 10000ms)
* **SetEventLoggingMaxQueueSize(int)**: Maximum events to queue before flushing
* **SetWaitForCountryLookupInit(bool)**: Wait for country lookup initialization
* **SetWaitForUserAgentInit(bool)**: Wait for user agent parsing initialization
* **SetDisableCountryLookup(bool)**: Disable automatic country detection
* **SetDisableUserAgentParsing(bool)**: Disable user agent parsing
* **SetDisableAllLogging(bool)**: Disable all event logging
* **SetInitTimeoutMs(int)**: Maximum time in milliseconds to wait for SDK initialization (default: 3000ms)
* **SetFallbackToStatsigApi(bool)**: Fallback to Statsig API when custom adapters fail (default: false)
* **SetEnableIDLists(bool)**: Enable ID list targeting
* **SetIDListsURL(string)**: Override the default ID lists endpoint
* **SetIDListsSyncIntervalMs(int)**: How often to sync ID lists (default: 60000ms)
* **SetGlobalCustomFields(Dictionary\<string, object>)**: Global custom fields for all events
* **SetSpecAdapterConfig(SpecAdapterConfig)**: Configure a custom spec source such as [Statsig Forward Proxy](/infrastructure/api_proxy/introduction)
* **SetProxyConfig(ProxyConfig)**: Configuration for connecting through a proxy server
{% /accordion %}

### 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](/infrastructure/api_proxy/introduction) or another custom spec source, use `SetSpecAdapterConfig(SpecAdapterConfig)`. If you only need custom Statsig endpoints, use `SetSpecsURL`, `SetLogEventURL`, and `SetIDListsURL`.

```csharp
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

```csharp
var specAdapterConfig = new SpecAdapterConfig(
    adapterType: "network_grpc_websocket",
    specsUrl: "http://forward-proxy.internal:50051"
);

var options = new StatsigOptionsBuilder()
    .SetSpecAdapterConfig(specAdapterConfig)
    .SetFallbackToStatsigApi(true)
    .Build();
```

{% accordion title="EvaluationOptions" %}
`EvaluationOptions` allows you to customize the behavior of feature flag evaluations:

```csharp
var options = new EvaluationOptions(disableExposureLogging: true);

var gateValue = statsig.CheckGate(user, "feature_gate", options);
var config = statsig.GetDynamicConfig(user, "product_config", options);
var experiment = statsig.GetExperiment(user, "button_test", options);
var layer = statsig.GetLayer(user, "user_prefs_layer", options);
```

## Options

* **DisableExposureLogging**: When `true`, prevents automatic exposure event logging for this evaluation. Useful when you want to evaluate a feature flag without affecting analytics or experiment results.

## Use cases

* **Internal Tools**: Check flag values for debugging without affecting user metrics
* **Conditional Logic**: Evaluate flags as part of complex logic where exposure should be logged manually later

When exposure logging is disabled, you can manually log exposures later using the manual exposure methods:

```csharp
var options = new EvaluationOptions(disableExposureLogging: true);
var gateValue = statsig.CheckGate(user, "feature_gate", options);

if (shouldLogExposure)
{
    statsig.ManuallyLogGateExposure(user, "feature_gate");
}
```
{% /accordion %}

## 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.

```csharp
await statsig.FlushEvents();

await statsig.Shutdown();

statsig.Dispose();
```

For shared instances:

```csharp
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.

```csharp
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.

Client SDKs also auto-generate a `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](/client/javascript-sdk-stable-id#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.

```csharp
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:

```csharp
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.

```csharp
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.

{% codetabs %}
```csharp Csharp
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();
```
{% /codetabs %}

## 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:

```csharp
await statsig.Initialize();
await statsig.FlushEvents();
await statsig.Shutdown();
```

Evaluation methods are synchronous for optimal performance:

```csharp
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:

```csharp
var sharedStatsig = Statsig.NewShared("server-secret-key");
await sharedStatsig.Initialize();

var statsig = Statsig.Shared();
```
