Replane 0.1.13

dotnet add package Replane --version 0.1.13
                    
NuGet\Install-Package Replane -Version 0.1.13
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Replane" Version="0.1.13" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Replane" Version="0.1.13" />
                    
Directory.Packages.props
<PackageReference Include="Replane" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Replane --version 0.1.13
                    
#r "nuget: Replane, 0.1.13"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Replane@0.1.13
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Replane&version=0.1.13
                    
Install as a Cake Addin
#tool nuget:?package=Replane&version=0.1.13
                    
Install as a Cake Tool

Replane .NET SDK

Official .NET SDK for Replane - Feature flags and remote configuration.

NuGet CI License Community

Installation

dotnet add package Replane

Quick Start

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);

Features

  • 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

Usage

Basic Configuration Access

// 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);

Complex Types

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

Context-Based Overrides

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);

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

Real-time Updates

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;

Default Values

Provide default values for when configs aren't loaded:

var replane = new ReplaneClient(new ReplaneClientOptions
{
    BaseUrl = "https://your-server.com",
    SdkKey = "your-key",
    Defaults = new Dictionary<string, object?>
    {
        ["feature-enabled"] = false,
        ["rate-limit"] = 100
    }
});

Required Configs

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();

Testing

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);
}

Testing with Overrides

[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();
}

Testing Segmentation

[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
}

Testing Config Changes

[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();
}

Testing Complex Types

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();
}

Dependency Injection

Both ReplaneClient and InMemoryReplaneClient implement the IReplaneClient interface, making it easy to swap implementations for testing or use with dependency injection:

ASP.NET Core Registration

// 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();

Using in Services

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
        });
    }
}

Testing with DI

[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();
}

Configuration Options

Option Type Default Description
BaseUrl string required Replane server URL
SdkKey string required SDK key for authentication
Context ReplaneContext null Default context for evaluations
Defaults Dictionary<string, object?> null Default 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
Logger IReplaneLogger null Custom logger implementation
Agent string null Agent identifier for User-Agent

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:

  • 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

Custom Logger

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()
});

Condition Operators

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

Error Handling

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

Examples

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.

Contributing

See CONTRIBUTING.md for development setup and contribution guidelines.

Community

Have questions or want to discuss Replane? Join the conversation in GitHub Discussions.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.13 181 12/24/2025
0.1.12 158 12/24/2025
0.1.11 163 12/24/2025
0.1.10 162 12/24/2025
0.1.8 166 12/22/2025
0.1.7 165 12/22/2025
0.1.6 162 12/22/2025
0.1.5 167 12/21/2025
0.1.3 149 12/21/2025
0.1.2 150 12/21/2025
0.1.1 148 12/21/2025
0.1.0 113 12/20/2025
0.0.2 117 12/20/2025