C#
The GrowthBook C# SDK supports all modern .NET platforms including .NET 6+, .NET Framework 4.6.1+, and .NET Standard 2.0+.
Installation
Install via NuGet Package Manager:
dotnet add package growthbook-c-sharp
Or via Package Manager Console:
Install-Package growthbook-c-sharp
Quick Start
Get started with GrowthBook in just a few steps:
using GrowthBook;
using Newtonsoft.Json.Linq;
// 1. Create a context with user attributes
var context = new Context
{
Enabled = true,
Attributes = new JObject
{
["id"] = "user-123",
["country"] = "US",
["plan"] = "premium"
}
};
// 2. Initialize GrowthBook
var gb = new GrowthBook.GrowthBook(context);
// 3. Load features from API (async)
await gb.LoadFeaturesAsync("https://cdn.growthbook.io", "sdk_abc123");
// 4. Evaluate features
if (gb.IsOn("new-dashboard"))
{
ShowNewDashboard();
}
var buttonColor = gb.GetFeatureValue("button-color", "blue");
var maxRetries = gb.GetFeatureValue("max-retries", 3);
Loading Features from API
Basic API Integration
using System.Net.Http;
using Newtonsoft.Json;
public class FeaturesResult
{
public HttpStatusCode Status { get; set; }
public IDictionary<string, Feature>? Features { get; set; }
public DateTimeOffset? DateUpdated { get; set; }
}
public async Task<GrowthBook.GrowthBook> InitializeGrowthBookAsync()
{
var context = new Context
{
Enabled = true,
Attributes = GetUserAttributes()
};
var gb = new GrowthBook.GrowthBook(context);
// Load features from API
using var httpClient = new HttpClient();
var url = "https://cdn.growthbook.io/api/features/sdk_abc123";
var response = await httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var featuresResult = JsonConvert.DeserializeObject<FeaturesResult>(content);
// Update context with loaded features
context.Features = featuresResult.Features;
gb.UpdateContext(context);
}
return gb;
}
private JObject GetUserAttributes()
{
return new JObject
{
["id"] = User.Identity.Name,
["email"] = User.Email,
["country"] = User.Country,
["plan"] = User.SubscriptionPlan
};
}
Streaming Updates
Enable real-time feature updates with Server-Sent Events (SSE):
using System.Threading;
using System.Threading.Tasks;
public class GrowthBookManager : IDisposable
{
private readonly GrowthBook.GrowthBook _gb;
private readonly Timer _refreshTimer;
private readonly HttpClient _httpClient;
public GrowthBookManager(Context context)
{
_gb = new GrowthBook.GrowthBook(context);
_httpClient = new HttpClient();
// Poll for updates every 60 seconds
_refreshTimer = new Timer(
async _ => await RefreshFeaturesAsync(),
null,
TimeSpan.Zero,
TimeSpan.FromSeconds(60)
);
}
private async Task RefreshFeaturesAsync()
{
try
{
var url = "https://cdn.growthbook.io/api/features/sdk_abc123";
var response = await _httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<FeaturesResult>(content);
// Update features
var context = _gb.GetContext();
context.Features = result.Features;
_gb.UpdateContext(context);
Console.WriteLine($"Features updated: {result.Features.Count} features loaded");
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to refresh features: {ex.Message}");
}
}
public GrowthBook.GrowthBook GetGrowthBook() => _gb;
public void Dispose()
{
_refreshTimer?.Dispose();
_httpClient?.Dispose();
}
}
User Attributes
Attributes are used for targeting and experiment assignment:
// Standard attributes
var attributes = new JObject
{
["id"] = "user-123",
["email"] = "user@example.com",
["country"] = "US",
["browser"] = "chrome"
};
// Custom business attributes
var attributes = new JObject
{
["id"] = user.Id,
["subscriptionTier"] = user.Tier,
["lifetimeValue"] = user.LifetimeValue,
["accountAge"] = (DateTime.Now - user.CreatedAt).Days,
["isHighValueCustomer"] = user.LifetimeValue > 1000,
["purchasedCategories"] = new JArray(user.Categories),
["enabledFeatures"] = new JArray(user.Features)
};
var context = new Context
{
Enabled = true,
Attributes = attributes
};
Evaluating Features
Feature Result Properties
var result = gb.EvalFeature("my-feature");
// Check if feature is enabled
if (result.On)
{
ShowNewFeature();
}
// Get feature value
var value = result.Value; // JToken
var typedValue = result.GetValue<string>(); // Typed accessor
// Check the source
switch (result.Source)
{
case FeatureResult.SourceId.DefaultValue:
// Using default value
break;
case FeatureResult.SourceId.Force:
// Forced value
break;
case FeatureResult.SourceId.Experiment:
// Value from experiment
var experiment = result.Experiment;
var experimentResult = result.ExperimentResult;
TrackExperiment(experiment, experimentResult);
break;
}
Generic Type Accessors
The SDK provides type-safe generic methods:
// Get feature values with type safety
var isEnabled = gb.GetFeatureValue<bool>("new-feature", false);
var buttonColor = gb.GetFeatureValue<string>("button-color", "blue");
var maxRetries = gb.GetFeatureValue<int>("max-retries", 3);
var timeout = gb.GetFeatureValue<double>("api-timeout", 5.0);
// Complex types
var config = gb.GetFeatureValue<Dictionary<string, object>>("app-config", null);
if (config != null)
{
var apiKey = config["apiKey"]?.ToString();
var maxConnections = Convert.ToInt32(config["maxConnections"]);
}
Running Experiments
Inline Experiments
var experiment = new Experiment
{
Key = "button-color-test",
Variations = new JArray { "blue", "red", "green" },
Weights = new List<double> { 0.5, 0.3, 0.2 }
};
var result = gb.Run(experiment);
if (result.InExperiment)
{
var color = result.GetValue<string>();
SetButtonColor(color);
// Track experiment view
TrackExperiment(experiment, result);
}
Experiment Configuration
var experiment = new Experiment
{
// Required
Key = "pricing-test",
Variations = new JArray { 9.99, 14.99, 19.99 },
// Optional configuration
Active = true,
Coverage = 0.8, // 80% of users
Weights = new List<double> { 0.5, 0.3, 0.2 },
// Targeting
Condition = JObject.Parse(@"{""country"": ""US"", ""plan"": ""premium""}"),
HashAttribute = "id",
// Sticky bucketing
BucketVersion = 1,
MinBucketVersion = 0,
DisableStickyBucketing = false
};
var result = gb.Run(experiment);
Encryption & Security
Encrypted Features
Enable encryption for sensitive feature configurations:
// Load encrypted features
var decryptionKey = Environment.GetEnvironmentVariable("GROWTHBOOK_DECRYPTION_KEY");
await gb.LoadFeaturesAsync(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk_abc123",
httpClient: new HttpClient(),
decryptionKey: decryptionKey
);
Secure Attributes
Hash sensitive attributes before sending them to GrowthBook:
using System.Security.Cryptography;
using System.Text;
public class SecureAttributeHelper
{
private readonly string _salt;
public SecureAttributeHelper(string salt)
{
_salt = salt;
}
public string HashAttribute(string value)
{
using var sha256 = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(value + _salt);
var hash = sha256.ComputeHash(bytes);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
public JObject BuildSecureAttributes(User user)
{
return new JObject
{
["id"] = user.Id,
// Hash sensitive attributes
["email"] = HashAttribute(user.Email),
["phone"] = HashAttribute(user.Phone),
// Non-sensitive attributes remain plain
["country"] = user.Country,
["plan"] = user.Plan
};
}
}
// Usage
var helper = new SecureAttributeHelper(
Environment.GetEnvironmentVariable("GROWTHBOOK_SECURE_ATTRIBUTE_SALT")
);
var context = new Context
{
Enabled = true,
Attributes = helper.BuildSecureAttributes(currentUser)
};
Security Best Practices
// Store keys securely in configuration
public class GrowthBookConfiguration
{
public string ClientKey { get; set; }
public string DecryptionKey { get; set; }
public string SecureAttributeSalt { get; set; }
}
// In Startup.cs or Program.cs
services.Configure<GrowthBookConfiguration>(
Configuration.GetSection("GrowthBook")
);
// Use in services
public class GrowthBookService
{
private readonly GrowthBookConfiguration _config;
public GrowthBookService(IOptions<GrowthBookConfiguration> config)
{
_config = config.Value;
}
public async Task<GrowthBook.GrowthBook> CreateGrowthBookAsync()
{
var context = new Context { Enabled = true };
var gb = new GrowthBook.GrowthBook(context);
await gb.LoadFeaturesAsync(
"https://cdn.growthbook.io",
_config.ClientKey,
decryptionKey: _config.DecryptionKey
);
return gb;
}
}
Security Recommendations:
- Never hardcode encryption keys or salts in source code
- Use configuration providers (appsettings.json, environment variables, Azure Key Vault)
- Rotate keys regularly and coordinate updates across all environments
- Use different keys for each environment (dev, staging, production)
Sticky Bucketing
Sticky bucketing ensures consistent experiment variations across sessions:
public interface IStickyBucketService
{
Task<StickyBucketAssignmentDoc> GetAssignmentsAsync(string attributeName, string attributeValue);
Task SaveAssignmentsAsync(StickyBucketAssignmentDoc doc);
Task<Dictionary<string, StickyBucketAssignmentDoc>> GetAllAssignmentsAsync(Dictionary<string, string> attributes);
}
public class StickyBucketAssignmentDoc
{
public string AttributeName { get; set; }
public string AttributeValue { get; set; }
public Dictionary<string, string> Assignments { get; set; }
}
Implementation Example
using Microsoft.Extensions.Caching.Memory;
public class MemoryStickyBucketService : IStickyBucketService
{
private readonly IMemoryCache _cache;
private readonly string _prefix = "gb_sticky_";
public MemoryStickyBucketService(IMemoryCache cache)
{
_cache = cache;
}
public Task<StickyBucketAssignmentDoc> GetAssignmentsAsync(
string attributeName,
string attributeValue)
{
var key = $"{_prefix}{attributeName}||{attributeValue}";
_cache.TryGetValue(key, out StickyBucketAssignmentDoc doc);
return Task.FromResult(doc);
}
public Task SaveAssignmentsAsync(StickyBucketAssignmentDoc doc)
{
var key = $"{_prefix}{doc.AttributeName}||{doc.AttributeValue}";
_cache.Set(key, doc, TimeSpan.FromDays(30));
return Task.CompletedTask;
}
public Task<Dictionary<string, StickyBucketAssignmentDoc>> GetAllAssignmentsAsync(
Dictionary<string, string> attributes)
{
var docs = new Dictionary<string, StickyBucketAssignmentDoc>();
foreach (var (attrName, attrValue) in attributes)
{
var doc = GetAssignmentsAsync(attrName, attrValue).Result;
if (doc != null)
{
var docKey = $"{doc.AttributeName}||{doc.AttributeValue}";
docs[docKey] = doc;
}
}
return Task.FromResult(docs);
}
}
// Configure in Startup.cs
services.AddMemoryCache();
services.AddSingleton<IStickyBucketService, MemoryStickyBucketService>();
// Use with GrowthBook
var context = new Context
{
Enabled = true,
StickyBucketService = stickyBucketService,
Attributes = attributes
};
Remote Evaluation
Remote evaluation evaluates feature flags on a secure server:
public class RemoteEvaluationService
{
private readonly HttpClient _httpClient;
private readonly string _apiHost;
private readonly string _clientKey;
public RemoteEvaluationService(HttpClient httpClient, string apiHost, string clientKey)
{
_httpClient = httpClient;
_apiHost = apiHost;
_clientKey = clientKey;
}
public async Task<Dictionary<string, FeatureResult>> EvaluateFeaturesAsync(JObject attributes)
{
var url = $"{_apiHost}/api/eval/{_clientKey}";
var payload = new
{
attributes = attributes
};
var response = await _httpClient.PostAsJsonAsync(url, payload);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsAsync<Dictionary<string, FeatureResult>>();
return result;
}
}
Async API Support
The C# SDK now provides comprehensive async APIs for non-blocking operations:
Async Feature Loading
// Load features asynchronously
await gb.LoadFeaturesAsync(apiHost, clientKey);
// Load features with custom HTTP client
using var httpClient = new HttpClient();
await gb.LoadFeaturesAsync(apiHost, clientKey, httpClient);
// Load encrypted features
await gb.LoadFeaturesAsync(apiHost, clientKey, httpClient, decryptionKey: "key_abc123");
Async Feature Refresh
// Refresh features in the background
await gb.RefreshFeaturesAsync();
// Refresh with custom timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await gb.RefreshFeaturesAsync(cts.Token);
Task-Based Patterns
All async operations return Task
or Task<T>
for seamless integration with async/await:
public async Task<IActionResult> Index()
{
var gb = GetGrowthBookInstance();
// Non-blocking feature evaluation
var features = await Task.Run(() => new
{
NewDashboard = gb.IsOn("new-dashboard"),
MaxItems = gb.GetFeatureValue("max-items", 10),
Theme = gb.GetFeatureValue("theme", "light")
});
return View(features);
}
Experiment Tracking
Implement tracking callbacks to send experiment data to your analytics:
public class GrowthBookWithTracking
{
private readonly GrowthBook.GrowthBook _gb;
private readonly IAnalyticsService _analytics;
public GrowthBookWithTracking(Context context, IAnalyticsService analytics)
{
_gb = new GrowthBook.GrowthBook(context);
_analytics = analytics;
// Subscribe to tracking events
context.TrackingCallback = TrackExperiment;
}
private void TrackExperiment(Experiment experiment, ExperimentResult result)
{
if (result.InExperiment)
{
_analytics.Track("experiment_viewed", new
{
experiment_id = experiment.Key,
variation_id = result.VariationId,
variation_value = result.Value,
user_id = result.HashValue
});
}
}
public GrowthBook.GrowthBook GetGrowthBook() => _gb;
}
Tracking with Experiments from Features
var featureResult = gb.EvalFeature("premium-feature");
if (featureResult.Source == FeatureResult.SourceId.Experiment)
{
var experiment = featureResult.Experiment;
var result = featureResult.ExperimentResult;
// Track to analytics
analytics.Track("experiment_viewed", new
{
experiment_id = experiment.Key,
variation_id = result.VariationId,
feature_id = result.FeatureId,
user_id = result.HashValue,
in_experiment = result.InExperiment,
hash_used = result.HashUsed
});
}
Troubleshooting & Logging
Diagnostic Logging
using Microsoft.Extensions.Logging;
public class GrowthBookLogger
{
private readonly ILogger _logger;
private readonly GrowthBook.GrowthBook _gb;
public GrowthBookLogger(GrowthBook.GrowthBook gb, ILogger<GrowthBookLogger> logger)
{
_gb = gb;
_logger = logger;
}
public void LogContext()
{
var context = _gb.GetContext();
_logger.LogInformation(
"GrowthBook Context: Enabled={Enabled}, Features={FeatureCount}, URL={Url}",
context.Enabled,
context.Features?.Count ?? 0,
context.Url
);
}
public void LogFeatureEvaluation(string featureKey, FeatureResult result)
{
_logger.LogDebug(
"Feature {FeatureKey}: On={On}, Source={Source}, Value={Value}",
featureKey,
result.On,
result.Source,
result.Value
);
}
public void LogExperiment(Experiment experiment, ExperimentResult result)
{
_logger.LogInformation(
"Experiment {ExperimentKey}: InExperiment={InExperiment}, VariationId={VariationId}, Value={Value}",
experiment.Key,
result.InExperiment,
result.VariationId,
result.Value
);
}
}
Common Issues
Features Not Loading
public async Task<bool> VerifyFeaturesLoadedAsync()
{
try
{
var context = _gb.GetContext();
if (context.Features == null || context.Features.Count == 0)
{
_logger.LogWarning("No features loaded in GrowthBook context");
return false;
}
_logger.LogInformation("Features loaded: {Count}", context.Features.Count);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error verifying features");
return false;
}
}
Decryption Failures
public async Task<bool> TestDecryptionAsync()
{
try
{
var decryptionKey = Environment.GetEnvironmentVariable("GROWTHBOOK_DECRYPTION_KEY");
if (string.IsNullOrEmpty(decryptionKey))
{
_logger.LogError("GROWTHBOOK_DECRYPTION_KEY not set");
return false;
}
await _gb.LoadFeaturesAsync(
"https://cdn.growthbook.io",
"sdk_abc123",
decryptionKey: decryptionKey
);
_logger.LogInformation("Successfully loaded encrypted features");
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load encrypted features");
return false;
}
}
Health Checks
using Microsoft.Extensions.Diagnostics.HealthChecks;
public class GrowthBookHealthCheck : IHealthCheck
{
private readonly GrowthBook.GrowthBook _gb;
public GrowthBookHealthCheck(GrowthBook.GrowthBook gb)
{
_gb = gb;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var gbContext = _gb.GetContext();
if (!gbContext.Enabled)
{
return Task.FromResult(
HealthCheckResult.Degraded("GrowthBook is disabled")
);
}
var featureCount = gbContext.Features?.Count ?? 0;
if (featureCount == 0)
{
return Task.FromResult(
HealthCheckResult.Degraded("No features loaded")
);
}
return Task.FromResult(
HealthCheckResult.Healthy($"{featureCount} features loaded")
);
}
catch (Exception ex)
{
return Task.FromResult(
HealthCheckResult.Unhealthy("GrowthBook health check failed", ex)
);
}
}
}
// Register in Startup.cs
services.AddHealthChecks()
.AddCheck<GrowthBookHealthCheck>("growthbook");
Integrations
ASP.NET Core
Integrate GrowthBook with ASP.NET Core for feature flags in your web application:
// Startup.cs or Program.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Register GrowthBook as singleton
services.AddSingleton<GrowthBook.GrowthBook>(sp =>
{
var context = new Context
{
Enabled = true,
Attributes = new JObject()
};
return new GrowthBook.GrowthBook(context);
});
// Register background service for feature refresh
services.AddHostedService<GrowthBookRefreshService>();
services.AddControllersWithViews();
}
}
// Background service for periodic refresh
public class GrowthBookRefreshService : BackgroundService
{
private readonly GrowthBook.GrowthBook _gb;
private readonly ILogger<GrowthBookRefreshService> _logger;
public GrowthBookRefreshService(
GrowthBook.GrowthBook gb,
ILogger<GrowthBookRefreshService> logger)
{
_gb = gb;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Initial load
await RefreshFeaturesAsync();
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken);
await RefreshFeaturesAsync();
}
}
private async Task RefreshFeaturesAsync()
{
try
{
using var httpClient = new HttpClient();
var url = "https://cdn.growthbook.io/api/features/sdk_abc123";
var response = await httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<FeaturesResult>(content);
var context = _gb.GetContext();
context.Features = result.Features;
_gb.UpdateContext(context);
_logger.LogInformation("Features refreshed: {Count}", result.Features.Count);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh features");
}
}
}
// Middleware for adding user context
public class GrowthBookMiddleware
{
private readonly RequestDelegate _next;
public GrowthBookMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, GrowthBook.GrowthBook gb)
{
// Build user attributes from HTTP context
var attributes = new JObject
{
["id"] = context.User.Identity?.Name,
["country"] = context.Request.Headers["CF-IPCountry"].FirstOrDefault(),
["userAgent"] = context.Request.Headers["User-Agent"].FirstOrDefault(),
["url"] = context.Request.Path.Value
};
// Update GrowthBook context for this request
var gbContext = gb.GetContext();
gbContext.Attributes = attributes;
gb.UpdateContext(gbContext);
// Store in HttpContext for controller access
context.Items["GrowthBook"] = gb;
await _next(context);
}
}
// Use in controller
public class HomeController : Controller
{
private readonly GrowthBook.GrowthBook _gb;
public HomeController(GrowthBook.GrowthBook gb)
{
_gb = gb;
}
public IActionResult Index()
{
// Use feature flags
var showNewDashboard = _gb.IsOn("new-dashboard");
var maxItems = _gb.GetFeatureValue("dashboard-max-items", 10);
// Track experiment
var colorResult = _gb.EvalFeature("dashboard-theme-color");
if (colorResult.Source == FeatureResult.SourceId.Experiment)
{
TrackExperiment(colorResult.Experiment, colorResult.ExperimentResult);
}
return View(new DashboardViewModel
{
ShowNewDashboard = showNewDashboard,
MaxItems = maxItems,
ThemeColor = colorResult.GetValue<string>()
});
}
private void TrackExperiment(Experiment experiment, ExperimentResult result)
{
// Track to your analytics service
Analytics.Track(User.Identity.Name, "experiment_viewed", new
{
experiment_id = experiment.Key,
variation_id = result.VariationId
});
}
}
Blazor Server
Use GrowthBook with Blazor Server for reactive feature flags:
// Program.cs
builder.Services.AddSingleton<GrowthBookService>();
builder.Services.AddScoped<UserGrowthBookService>();
// GrowthBookService.cs
public class GrowthBookService
{
private readonly GrowthBook.GrowthBook _gb;
private readonly ILogger<GrowthBookService> _logger;
public GrowthBookService(ILogger<GrowthBookService> logger)
{
_logger = logger;
var context = new Context { Enabled = true };
_gb = new GrowthBook.GrowthBook(context);
// Start background refresh
_ = RefreshFeaturesAsync();
}
public GrowthBook.GrowthBook GetGrowthBook() => _gb;
public async Task RefreshFeaturesAsync()
{
try
{
using var httpClient = new HttpClient();
await _gb.LoadFeaturesAsync(
"https://cdn.growthbook.io",
"sdk_abc123",
httpClient
);
_logger.LogInformation("Features loaded");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load features");
}
}
public event EventHandler FeaturesUpdated;
protected virtual void OnFeaturesUpdated()
{
FeaturesUpdated?.Invoke(this, EventArgs.Empty);
}
}
// Blazor component
@page "/dashboard"
@inject GrowthBookService GrowthBookService
@implements IDisposable
<h3>Dashboard</h3>
@if (_newDashboard)
{
<NewDashboardComponent MaxItems="@_maxItems" />
}
else
{
<LegacyDashboardComponent MaxItems="@_maxItems" />
}
@code {
private bool _newDashboard;
private int _maxItems;
protected override void OnInitialized()
{
UpdateFeatures();
GrowthBookService.FeaturesUpdated += OnFeaturesUpdated;
}
private void OnFeaturesUpdated(object sender, EventArgs e)
{
UpdateFeatures();
StateHasChanged();
}
private void UpdateFeatures()
{
var gb = GrowthBookService.GetGrowthBook();
_newDashboard = gb.IsOn("new-dashboard");
_maxItems = gb.GetFeatureValue("max-items", 20);
}
public void Dispose()
{
GrowthBookService.FeaturesUpdated -= OnFeaturesUpdated;
}
}
Supported Features
FeaturesAll versions
ExperimentationAll versions
Sticky Bucketing≥ v1.1.0
Prerequisites≥ v1.1.0
Saved Group References≥ v1.1.0
Encrypted Features≥ v1.0.0
Streaming≥ v1.0.0
v2 Hashing≥ v1.0.0
SemVer Targeting≥ v1.0.0