OnionSwitch 0.0.1-beta.1

This is a prerelease version of OnionSwitch.
dotnet add package OnionSwitch --version 0.0.1-beta.1
                    
NuGet\Install-Package OnionSwitch -Version 0.0.1-beta.1
                    
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="OnionSwitch" Version="0.0.1-beta.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OnionSwitch" Version="0.0.1-beta.1" />
                    
Directory.Packages.props
<PackageReference Include="OnionSwitch" />
                    
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 OnionSwitch --version 0.0.1-beta.1
                    
#r "nuget: OnionSwitch, 0.0.1-beta.1"
                    
#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 OnionSwitch@0.0.1-beta.1
                    
#: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=OnionSwitch&version=0.0.1-beta.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=OnionSwitch&version=0.0.1-beta.1&prerelease
                    
Install as a Cake Tool

OnionSwitch

NuGet

OnionSwitch is the official .NET client for Onion Switch — a feature flag platform for kill switches, gradual rollouts, and user targeting. Inject IFlags into your services to evaluate flags from a locally cached snapshot that refreshes in the background.

In the Onion Switch dashboard, environments are labeled namespace; in the SDK and API the same identifier is environmentId (a GUID).

Why Onion Switch

  • Background refresh — a hosted BackgroundService polls the API on a configurable interval (default 30 seconds) and updates an in-memory snapshot.
  • User targeting — evaluate flags per user with included/excluded users and groups, plus percentage rollouts.
  • Flexible authentication — API key or OAuth 2.0 Client Credentials (scopes, audience, or custom token parameters).
  • IConfiguration integration — flag state is merged into the OnionSwitch configuration section; set flag defaults in appsettings.json before the first API fetch.
  • FlagsFactory — build an IFlags instance from FlagDto[] for unit tests, CI, and offline evaluation without HTTP or polling.

Requirements

  • .NET 10+ (net10.0)
  • A running Onion Switch instance (hosted or self-hosted)
  • Environment ID (GUID) from the dashboard
  • API key or OAuth client credentials when the environment requires authenticated reads for private flags (anonymous reads are supported when your environment allows them)

Installation

dotnet add package OnionSwitch

Quick start (hosted — ASP.NET Core)

Register the client in Program.cs, configure authentication, and inject IFlags where you need evaluations:

using Syntelise.OnionSwitch;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOnionSwitch(
    builder.Configuration,
    backendAddress: "https://your-onion-switch-api",
    environmentId: Guid.Parse("11111111-1111-1111-1111-111111111111"),
    configure: options =>
    {
        options.SetApiKeyAuthentication("your-api-key");
        options.UpdatePeriod = TimeSpan.FromSeconds(30);
    });

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();
app.Run();

Evaluate a flag in a service or controller:

using Syntelise.OnionSwitch;

public sealed class CheckoutController(IFlags flags) : ControllerBase
{
    [HttpGet("/checkout")]
    public async Task<IActionResult> Index(CancellationToken cancellationToken)
    {
        if (await flags.IsEnabledAsync("my-feature", cancellationToken))
        {
            return Ok("New checkout flow");
        }

        return Ok("Legacy checkout flow");
    }
}

Authentication

Configure exactly one authenticator inside the configure callback. If you call both setters, the last call wins.

API Key

Sends Authorization: ApiKey <token> on every request to the Onion Switch API.

builder.Services.AddOnionSwitch(
    builder.Configuration,
    backendAddress: "https://your-onion-switch-api",
    environmentId: Guid.Parse("11111111-1111-1111-1111-111111111111"),
    configure: options =>
    {
        options.SetApiKeyAuthentication("your-api-key");
    });

OAuth 2.0 Client Credentials

Request a bearer token from your identity provider. Use scopes for standard OAuth providers:

builder.Services.AddOnionSwitch(
    builder.Configuration,
    backendAddress: "https://your-onion-switch-api",
    environmentId: Guid.Parse("11111111-1111-1111-1111-111111111111"),
    configure: options =>
    {
        options.SetClientCredentialsAuthentication(
            authorizationEndpoint: "https://idp.example.com/connect/token",
            clientId: "your-client-id",
            clientSecret: "your-client-secret",
            scopes: new[] { "flags.read" });
    });

For Auth0-style providers, pass an audience instead of scopes:

options.SetClientCredentialsAuthentication(
    authorizationEndpoint: "https://idp.example.com/connect/token",
    clientId: "your-client-id",
    clientSecret: "your-client-secret",
    audience: "onion-switch-api");

For non-standard token requests, use ClientCredentialsParam[] — see the Configuration docs.

Evaluating flags

Inject IFlags (scoped) and choose the overload that fits your context:

using Syntelise.OnionSwitch;

public sealed class FeatureService(IFlags flags)
{
    public async Task RunAsync(CancellationToken cancellationToken)
    {
        // Default targeting from DI (IUserTargeting registered via SetUserTargeting)
        var enabled = await flags.IsEnabledAsync("feature-name", cancellationToken);

        // Explicit UserTargeting — synchronous, no network I/O
        var targeting = new UserTargeting(UserName: "alice", UserGroups: ["beta"]);
        var forAlice = flags.IsEnabledAsync("feature-name", targeting);

        // All flags for the ambient user
        var allForCurrentUser = await flags.GetAllAsync(cancellationToken);

        // All flags for a specific user — synchronous
        var allForAlice = flags.GetAllAsync(targeting);
    }
}
Method Returns When to use
IsEnabledAsync(name, ct) ValueTask<bool> HTTP requests with ambient IUserTargeting
IsEnabledAsync(name, targeting) bool Background jobs, explicit user/tenant context
GetAllAsync(ct) ValueTask<UserFlag[]> Debug endpoints, shipping a snapshot to a client
GetAllAsync(targeting) UserFlag[] Bulk evaluation for a known user

UserFlag is a record: (string Name, bool Enabled).

Unknown flag names resolve to false (no exception). Evaluations always read the in-memory snapshot — they never await the network.

User targeting (ASP.NET Core)

Targeting rules (included/excluded users and groups, percentage rollouts) are applied in the same order as the server. See Targeting concepts.

Simple: UserTargeting record

Pass a UserTargeting instance to the explicit overload when you know the user at evaluation time:

var targeting = new UserTargeting(
    UserName: "user123",
    UserGroups: ["premium", "beta-testers"]);

var isEnabled = flags.IsEnabledAsync("premium-feature", targeting);
var allFlags = flags.GetAllAsync(targeting);

Advanced: custom IUserTargeting

Register a type that resolves the current user from HttpContext, your auth system, or another source:

using Syntelise.OnionSwitch;
using Syntelise.OnionSwitch.Targeting;

public sealed class HttpContextUserTargeting(IHttpContextAccessor httpContextAccessor) : IUserTargeting
{
    public ValueTask<string?> GetUserNameAsync(CancellationToken cancellationToken) =>
        new(httpContextAccessor.HttpContext?.User?.Identity?.Name);

    public ValueTask<string[]> GetGroupsAsync(CancellationToken cancellationToken)
    {
        var groups = httpContextAccessor.HttpContext?.User?.Claims
            .Where(c => c.Type == "groups")
            .Select(c => c.Value)
            .ToArray() ?? [];

        return new ValueTask<string[]>(groups);
    }
}

// Registration
builder.Services.AddHttpContextAccessor();

builder.Services.AddOnionSwitch(
    builder.Configuration,
    backendAddress: "https://your-onion-switch-api",
    environmentId: Guid.Parse("11111111-1111-1111-1111-111111111111"),
    configure: options =>
    {
        options.SetApiKeyAuthentication("your-api-key");
        options.SetUserTargeting<HttpContextUserTargeting>();
    });

// Usage — ambient overload uses HttpContext automatically
public sealed class MyService(IFlags flags)
{
    public async Task DoSomethingAsync(CancellationToken cancellationToken)
    {
        if (await flags.IsEnabledAsync("my-feature", cancellationToken))
        {
            // Feature enabled for the current user
        }
    }
}

Targeting rules:

  • Included users and groups turn a flag on for matching identities.
  • Excluded users and groups turn a flag off before includes are checked.
  • Percentage rollouts apply after include/exclude lists.
  • Without a custom IUserTargeting, the SDK uses an internal empty targeting provider (anonymous user). Flags with any targeting rule evaluate to false for the ambient overload — register IUserTargeting or pass explicit UserTargeting.

Ensure ASP.NET Core authentication runs before endpoints that rely on ambient targeting (UseAuthentication before flag checks).

More detail: Pass user context.

Configuration & appsettings

Refresh period

Control how often the background service polls the API:

configure: options =>
{
    options.UpdatePeriod = TimeSpan.FromSeconds(60); // minimum: 1 second; default: 30 seconds
}

Evaluations use the last loaded snapshot regardless of the period. Lower values reduce staleness but increase API traffic.

Connection settings (separate from flags)

Keep backend URL, environment ID, and secrets in their own configuration section. Do not put them under OnionSwitch — that section is bound as a dictionary of flag names (FlagsOptions), so keys like Backend would be treated as flag definitions.

Use any section name you prefer; OnionSwitchClient is a common choice:

{
  "OnionSwitchClient": {
    "Backend": "https://your-onion-switch-api",
    "EnvironmentId": "11111111-1111-1111-1111-111111111111",
    "ApiKey": "your-api-key"
  }
}

Read those values when registering the SDK:

builder.Services.AddOnionSwitch(
    builder.Configuration,
    backendAddress: builder.Configuration["OnionSwitchClient:Backend"]!,
    environmentId: Guid.Parse(builder.Configuration["OnionSwitchClient:EnvironmentId"]!),
    configure: options =>
    {
        options.SetApiKeyAuthentication(builder.Configuration["OnionSwitchClient:ApiKey"]!);
    });

Override with environment variables using ASP.NET Core convention (e.g. OnionSwitchClient__Backend, OnionSwitchClient__ApiKey).

Flag defaults under OnionSwitch

The SDK merges flag state into IConfiguration under the OnionSwitch section only. You can provide defaults there that apply before the first successful API fetch (and remain for keys the API has not yet returned):

{
  "OnionSwitch": {
    "checkout-v2": {
      "Enabled": "true"
    }
  }
}

Per-flag keys follow this layout:

Key Meaning
OnionSwitch:{name}:Enabled Master on/off switch

Evaluate flags with IFlags — do not bind flag sections to custom options types. For the full key layout and advanced defaults, see Default flag configuration.

Worker services / background apps

Use the same AddOnionSwitch registration on Host.CreateApplicationBuilder. There is no HttpContext in workers — pass explicit UserTargeting or register a custom IUserTargeting with a stable synthetic identity.

using Syntelise.OnionSwitch;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddOnionSwitch(
    builder.Configuration,
    backendAddress: builder.Configuration["OnionSwitchClient:Backend"]!,
    environmentId: Guid.Parse(builder.Configuration["OnionSwitchClient:EnvironmentId"]!),
    configure: options =>
    {
        options.SetApiKeyAuthentication(builder.Configuration["OnionSwitchClient:ApiKey"]!);
        // options.SetUserTargeting<ProcessUserTargeting>(); // fixed worker identity
    });

builder.Services.AddHostedService<OutboundSyncWorker>();

await builder.Build().RunAsync();

IFlags is scoped. Hosted services are singletons — resolve IFlags from a scope per unit of work:

using Syntelise.OnionSwitch;

internal sealed class OutboundSyncWorker(IServiceScopeFactory scopeFactory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await using var scope = scopeFactory.CreateAsyncScope();
            var flags = scope.ServiceProvider.GetRequiredService<IFlags>();

            var batchOn = flags.IsEnabledAsync(
                "outbound-batch-v2",
                new UserTargeting("worker:outbound-sync", []));

            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

Do not inject IFlags directly into a BackgroundService constructor — that creates a captive scoped dependency.

Testing & offline evaluation

Use FlagsFactory when you already have flag definitions and do not need HTTP polling or AddOnionSwitch:

using Syntelise.OnionSwitch;
using Syntelise.OnionSwitch.Http.Dto;

FlagDto[] flagDtos =
[
    new FlagDto { Name = "checkout-v2", Enabled = true }
];

IFlags flags = FlagsFactory.Create(flagDtos, targeting: null);

var enabled = flags.IsEnabledAsync("checkout-v2", new UserTargeting("user-42", []));

Pass targeting: null to use the built-in empty ambient targeting (anonymous). Pass an IUserTargeting implementation when you need the async ambient overloads in tests.

Typical uses: unit tests, CI pipelines, snapshot files deserialized from the API, or any scenario where the flag definitions are already in memory.

Caching & updates

On startup, the SDK performs one synchronous HTTP fetch when the configuration root is built. A BackgroundService then polls on UpdatePeriod (default 30 seconds) and reloads the in-memory snapshot when the request succeeds.

Evaluations never block on the network — they read whatever snapshot is currently loaded. After a dashboard change, expect up to one refresh interval before all app instances reflect the new value (eventual consistency). Failed background polls keep the last good snapshot; only a successful response replaces it.

Tune refresh behavior and multi-instance semantics: Caching and updates.

Documentation

Full product and SDK documentation: https://onionswitch.com/documentation

Topic Link
What is Onion Switch https://onionswitch.com/documentation/introduction/what-is-onion-switch
Developer quickstart https://onionswitch.com/documentation/getting-started/quickstart-developer
.NET: Installation https://onionswitch.com/documentation/sdks/dotnet/installation
.NET: Configuration https://onionswitch.com/documentation/sdks/dotnet/configuration
.NET: Evaluating flags https://onionswitch.com/documentation/sdks/dotnet/evaluating-flags
.NET: ASP.NET Core https://onionswitch.com/documentation/sdks/dotnet/integrate-with-aspnetcore
.NET: Worker services https://onionswitch.com/documentation/sdks/dotnet/integrate-with-worker-services
.NET: Offline / FlagsFactory https://onionswitch.com/documentation/sdks/dotnet/test-flags-locally
.NET: Errors & fallbacks https://onionswitch.com/documentation/sdks/dotnet/handle-errors-and-fallbacks
.NET: App configuration https://onionswitch.com/documentation/sdks/dotnet/use-flags-as-app-configuration
Pass user context https://onionswitch.com/documentation/integrate/pass-user-context
Self-hosted overview https://onionswitch.com/documentation/self-hosted/overview
Troubleshooting (SDK) https://onionswitch.com/documentation/troubleshoot/sdk

License

MIT — See LICENSE.

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.

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.0.1-beta.1 111 5/30/2026