Replane SDK for .NET - Feature flags and remote configuration client
$ dotnet add package ReplaneOfficial .NET SDK for Replane - Feature flags and remote configuration.
dotnet add package Replane
using Replane;
// Create and connect
await using var replane = new ReplaneClient(new ReplaneClientOptions
{
BaseUrl = "https://your-replane-server.com",
SdkKey = "your-sdk-key"
});
await replane.ConnectAsync();
// Get a config value
var featureEnabled = replane.Get<bool>("feature-enabled");
var maxItems = replane.Get<int>("max-items", defaultValue: 100);
// Get typed config values
var enabled = replane.Get<bool>("feature-enabled");
var limit = replane.Get<int>("rate-limit");
var apiKey = replane.Get<string>("api-key");
// With default values
var timeout = replane.Get<int>("timeout-ms", defaultValue: 5000);
Configs can store complex objects that are deserialized on demand:
// Define your config type
public record ThemeConfig
{
public bool DarkMode { get; init; }
public string PrimaryColor { get; init; } = "";
public int FontSize { get; init; }
}
// Get complex config
var theme = replane.Get<ThemeConfig>("theme");
Console.WriteLine($"Dark mode: {theme.DarkMode}, Color: {theme.PrimaryColor}");
// Works with overrides too - different themes for different users
var userTheme = replane.Get<ThemeConfig>("theme", new ReplaneContext { ["plan"] = "premium" });Evaluate configs based on user context:
// Create context for override evaluation
var context = new ReplaneContext
{
["user_id"] = "user-123",
["plan"] = "premium",
["region"] = "us-east"
};
// Get config with context
var premiumFeature = replane.Get<bool>("premium-feature", context);Set default context that's merged with per-call context:
var replane = new ReplaneClient(new ReplaneClientOptions
{
BaseUrl = "https://your-server.com",
SdkKey = "your-key",
Context = new ReplaneContext
{
["app_version"] = "2.0.0",
["platform"] = "ios"
}
});Subscribe to config changes using the ConfigChanged event:
// Subscribe to all config changes
replane.ConfigChanged += (sender, e) =>
{
Console.WriteLine($"Config '{e.ConfigName}' updated");
};
// Get typed value from the event
replane.ConfigChanged += (sender, e) =>
{
if (e.ConfigName == "feature-flag")
{
var enabled = e.GetValue<bool>();
Console.WriteLine($"Feature flag changed to: {enabled}");
}
};
// Works with complex types too
replane.ConfigChanged += (sender, e) =>
{
if (e.ConfigName == "theme")
{
var theme = e.GetValue<ThemeConfig>();
Console.WriteLine($"Theme updated: dark={theme?.DarkMode}");
}
};
// Unsubscribe when needed
void OnConfigChanged(object? sender, ConfigChangedEventArgs e)
{
Console.WriteLine($"Config changed: {e.ConfigName}");
}
replane.ConfigChanged += OnConfigChanged;
// Later...
replane.ConfigChanged -= OnConfigChanged;Provide fallback values for when configs aren't loaded:
var replane = new ReplaneClient(new ReplaneClientOptions
{
BaseUrl = "https://your-server.com",
SdkKey = "your-key",
Fallbacks = new Dictionary<string, object?>
{
["feature-enabled"] = false,
["rate-limit"] = 100
}
});Ensure specific configs are present on initialization:
var replane = new ReplaneClient(new ReplaneClientOptions
{
BaseUrl = "https://your-server.com",
SdkKey = "your-key",
Required = ["essential-config", "api-endpoint"]
});
// ConnectAsync will throw if required configs are missing
await replane.ConnectAsync();Use the in-memory client for unit tests:
using Replane.Testing;
[Fact]
public void TestFeatureFlag()
{
// Create test client with initial configs
using var client = TestClient.Create(new Dictionary<string, object?>
{
["feature-enabled"] = true,
["max-items"] = 50
});
// Use like the real client
client.Get<bool>("feature-enabled").Should().BeTrue();
client.Get<int>("max-items").Should().Be(50);
}[Fact]
public void TestOverrides()
{
using var client = TestClient.Create();
// Set up config with overrides
client.SetConfigWithOverrides(
name: "premium-feature",
value: false,
overrides: [
new OverrideData
{
Name = "premium-users",
Conditions = [
new ConditionData
{
Operator = "equals",
Property = "plan",
Expected = "premium"
}
],
Value = true
}
]);
// Test with different contexts
client.Get<bool>("premium-feature", new ReplaneContext { ["plan"] = "free" })
.Should().BeFalse();
client.Get<bool>("premium-feature", new ReplaneContext { ["plan"] = "premium" })
.Should().BeTrue();
}[Fact]
public void TestABTest()
{
using var client = TestClient.Create();
client.SetConfigWithOverrides(
name: "ab-test",
value: "control",
overrides: [
new OverrideData
{
Name = "treatment-group",
Conditions = [
new ConditionData
{
Operator = "segmentation",
Property = "user_id",
FromPercentage = 0,
ToPercentage = 50,
Seed = "experiment-seed"
}
],
Value = "treatment"
}
]);
// Result is deterministic for each user
var result = client.Get<string>("ab-test", new ReplaneContext { ["user_id"] = "user-123" });
// Will consistently be either "control" or "treatment" for this user
}[Fact]
public void TestConfigChangeEvent()
{
using var client = TestClient.Create();
var receivedEvents = new List<ConfigChangedEventArgs>();
client.ConfigChanged += (sender, e) => receivedEvents.Add(e);
client.Set("feature", true);
client.Set("feature", false);
receivedEvents.Should().HaveCount(2);
receivedEvents[0].GetValue<bool>().Should().BeTrue();
receivedEvents[1].GetValue<bool>().Should().BeFalse();
}public record FeatureFlags
{
public bool NewUI { get; init; }
public List<string> EnabledModules { get; init; } = [];
}
[Fact]
public void TestComplexType()
{
var flags = new FeatureFlags
{
NewUI = true,
EnabledModules = ["dashboard", "analytics"]
};
using var client = TestClient.Create(new Dictionary<string, object?>
{
["features"] = flags
});
var result = client.Get<FeatureFlags>("features");
result!.NewUI.Should().BeTrue();
result.EnabledModules.Should().Contain("dashboard");
}
[Fact]
public void TestComplexTypeWithOverrides()
{
using var client = TestClient.Create();
var defaultTheme = new ThemeConfig { DarkMode = false, PrimaryColor = "#000", FontSize = 12 };
var premiumTheme = new ThemeConfig { DarkMode = true, PrimaryColor = "#FFD700", FontSize = 16 };
client.SetConfigWithOverrides(
name: "theme",
value: defaultTheme,
overrides: [
new OverrideData
{
Name = "premium-theme",
Conditions = [
new ConditionData { Operator = "equals", Property = "plan", Expected = "premium" }
],
Value = premiumTheme
}
]);
client.Get<ThemeConfig>("theme", new ReplaneContext { ["plan"] = "free" })!
.DarkMode.Should().BeFalse();
client.Get<ThemeConfig>("theme", new ReplaneContext { ["plan"] = "premium" })!
.DarkMode.Should().BeTrue();
}| Option | Type | Default | Description |
|---|---|---|---|
BaseUrl | string | required | Replane server URL |
SdkKey | string | required | SDK key for authentication |
Context | ReplaneContext | null | Default context for evaluations |
Fallbacks | Dictionary<string, object?> | null | Fallback values |
Required | IReadOnlyList<string> | null | Required config names |
RequestTimeoutMs | int | 2000 | HTTP request timeout |
InitializationTimeoutMs | int | 5000 | Initial connection timeout |
RetryDelayMs | int | 200 | Initial retry delay |
InactivityTimeoutMs | int | 30000 | SSE inactivity timeout |
HttpClient | HttpClient | null | Custom HttpClient |
Debug | bool | false | Enable debug logging |
Enable debug logging to troubleshoot issues:
var replane = new ReplaneClient(new ReplaneClientOptions
{
BaseUrl = "https://your-server.com",
SdkKey = "your-key",
Debug = true
});This outputs detailed logs including:
Get() call with config name, context, and resultExample output:
[DEBUG] Replane: Initializing ReplaneClient with options:
[DEBUG] Replane: BaseUrl: https://your-server.com
[DEBUG] Replane: SdkKey: your...key
[DEBUG] Replane: Connecting to SSE: https://your-server.com/api/sdk/v1/replication/stream
[DEBUG] Replane: SSE event received: type=init
[DEBUG] Replane: Initialization complete: 5 configs loaded
[DEBUG] Replane: Get<Boolean>("feature-flag") called
[DEBUG] Replane: Config "feature-flag" found, base value: false, overrides: 1
[DEBUG] Replane: Evaluating override #0 (conditions: property(plan equals "premium"))
[DEBUG] Replane: Condition: property "plan" ("premium") equals "premium" => Matched
[DEBUG] Replane: Override #0 matched, returning: true
Provide your own logger implementation:
public class MyLogger : IReplaneLogger
{
public void Log(LogLevel level, string message, Exception? exception = null)
{
// Forward to your logging framework
_logger.Log(MapLevel(level), exception, message);
}
}
var replane = new ReplaneClient(options, new MyLogger());The SDK supports the following condition operators for overrides:
| Operator | Description |
|---|---|
equals | Exact match |
in | Value is in list |
not_in | Value is not in list |
less_than | Less than comparison |
less_than_or_equal | Less than or equal |
greater_than | Greater than comparison |
greater_than_or_equal | Greater than or equal |
segmentation | Percentage-based bucketing |
and | All conditions must match |
or | Any condition must match |
not | Negate a condition |
try
{
await replane.ConnectAsync();
var value = replane.Get<string>("my-config");
}
catch (AuthenticationException)
{
// Invalid SDK key
}
catch (ConfigNotFoundException ex)
{
// Config doesn't exist
Console.WriteLine($"Config not found: {ex.ConfigName}");
}
catch (ReplaneTimeoutException ex)
{
// Operation timed out
Console.WriteLine($"Timeout after {ex.TimeoutMs}ms");
}
catch (ReplaneException ex)
{
// Other errors
Console.WriteLine($"Error [{ex.Code}]: {ex.Message}");
}See the examples directory for complete working examples:
| Example | Description |
|---|---|
| BasicUsage | Simple console app with basic config reading |
| ConsoleWithOverrides | Context-based overrides and user segmentation |
| BackgroundWorker | Long-running service with real-time config updates |
| WebApiIntegration | ASP.NET Core Web API with middleware and DI |
| UnitTesting | Unit testing with the in-memory test client |
Each example is self-contained and can be copied and run independently.
See CONTRIBUTING.md for development setup and contribution guidelines.
Have questions or want to discuss Replane? Join the conversation in GitHub Discussions.
MIT