Skip to main content

C#

The GrowthBook C# SDK supports all modern .NET platforms including .NET 6+, .NET Framework 4.6.1+, and .NET Standard 2.0+.

C# SDK Resources
v1.1.0
growthbook-c-sharpNuGetC# example appGet help on Slack

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