Official .NET SDK for Replane - Feature flags and remote configuration.
dotnet add package Replaneusing Replane;
// Create client and connect
await using var replane = new ReplaneClient();
await replane.ConnectAsync(new ConnectOptions
{
BaseUrl = "https://cloud.replane.dev", // or your self-hosted URL
SdkKey = "your-sdk-key"
});
// Get a config value
var featureEnabled = replane.Get<bool>("feature-enabled");
var maxItems = replane.Get<int>("max-items", defaultValue: 100);Tip: Get started instantly with Replane Cloud — no infrastructure required.
- Real-time updates via Server-Sent Events (SSE)
- Client-side evaluation - context never leaves your application
- Gradual rollouts with percentage-based segmentation
- Override rules with flexible conditions
- Type-safe configuration access
- Async/await support throughout
- In-memory test client for unit testing
// 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 default values for when configs aren't loaded:
var replane = new ReplaneClient(new ReplaneClientOptions
{
Defaults = new Dictionary<string, object?>
{
["feature-enabled"] = false,
["rate-limit"] = 100
}
});Ensure specific configs are present on initialization:
var replane = new ReplaneClient(new ReplaneClientOptions
{
Required = ["essential-config", "api-endpoint"]
});
// ConnectAsync will throw if required configs are missing
await replane.ConnectAsync(new ConnectOptions
{
BaseUrl = "https://your-server.com",
SdkKey = "your-key"
});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();
}Both ReplaneClient and InMemoryReplaneClient implement the IReplaneClient interface, making it easy to swap implementations for testing or use with dependency injection:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Register Replane client as the interface
builder.Services.AddSingleton<IReplaneClient>(sp =>
{
var client = new ReplaneClient(new ReplaneClientOptions
{
BaseUrl = builder.Configuration["Replane:BaseUrl"]!,
SdkKey = builder.Configuration["Replane:SdkKey"]!
});
return client;
});
var app = builder.Build();
// Connect on startup
var replane = app.Services.GetRequiredService<IReplaneClient>();
if (replane is ReplaneClient realClient)
{
await realClient.ConnectAsync();
}
// Use in controllers/services
app.MapGet("/api/items", (IReplaneClient replane) =>
{
var maxItems = replane.Get<int>("max-items", defaultValue: 100);
return Results.Ok(new { maxItems });
});
app.Run();public class FeatureService
{
private readonly IReplaneClient _replane;
public FeatureService(IReplaneClient replane)
{
_replane = replane;
}
public bool IsFeatureEnabled(string userId)
{
return _replane.Get<bool>("new-feature", new ReplaneContext
{
["user_id"] = userId
});
}
}[Fact]
public void TestFeatureService()
{
// Create test client implementing IReplaneClient
using var testClient = TestClient.Create(new Dictionary<string, object?>
{
["new-feature"] = true
});
// Inject into service
var service = new FeatureService(testClient);
// Test the service
service.IsFeatureEnabled("user-123").Should().BeTrue();
}Options passed to the constructor. Connection options are provided via ConnectAsync.
| Option | Type | Default | Description |
|---|---|---|---|
Context |
ReplaneContext |
null |
Default context for evaluations |
Defaults |
Dictionary<string, object?> |
null |
Default values |
Required |
IReadOnlyList<string> |
null |
Required config names |
HttpClient |
HttpClient |
null |
Custom HttpClient |
Debug |
bool |
false |
Enable debug logging |
Logger |
IReplaneLogger |
null |
Custom logger implementation |
Connection options passed to ConnectAsync.
| Option | Type | Default | Description |
|---|---|---|---|
BaseUrl |
string |
required | Replane server URL |
SdkKey |
string |
required | SDK key for authentication |
RequestTimeoutMs |
int |
2000 |
HTTP request timeout |
ConnectionTimeoutMs |
int |
5000 |
Initial connection timeout |
RetryDelayMs |
int |
200 |
Initial retry delay |
InactivityTimeoutMs |
int |
30000 |
SSE inactivity timeout |
Agent |
string |
null |
Agent identifier |
Enable debug logging to troubleshoot issues:
var replane = new ReplaneClient(new ReplaneClientOptions
{
Debug = true
});
await replane.ConnectAsync(new ConnectOptions
{
BaseUrl = "https://your-server.com",
SdkKey = "your-key"
});This outputs detailed logs including:
- Client initialization with all options
- SSE connection lifecycle (connect, reconnect, disconnect)
- Every
Get()call with config name, context, and result - Override evaluation details (which conditions matched/failed)
- Raw SSE event data
Example 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(new ReplaneClientOptions
{
BaseUrl = "https://your-server.com",
SdkKey = "your-key",
Logger = 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