OnionSwitch 0.0.1-beta.1
dotnet add package OnionSwitch --version 0.0.1-beta.1
NuGet\Install-Package OnionSwitch -Version 0.0.1-beta.1
<PackageReference Include="OnionSwitch" Version="0.0.1-beta.1" />
<PackageVersion Include="OnionSwitch" Version="0.0.1-beta.1" />
<PackageReference Include="OnionSwitch" />
paket add OnionSwitch --version 0.0.1-beta.1
#r "nuget: OnionSwitch, 0.0.1-beta.1"
#:package OnionSwitch@0.0.1-beta.1
#addin nuget:?package=OnionSwitch&version=0.0.1-beta.1&prerelease
#tool nuget:?package=OnionSwitch&version=0.0.1-beta.1&prerelease
OnionSwitch
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
BackgroundServicepolls 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).
IConfigurationintegration — flag state is merged into theOnionSwitchconfiguration section; set flag defaults inappsettings.jsonbefore the first API fetch.FlagsFactory— build anIFlagsinstance fromFlagDto[]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 tofalsefor the ambient overload — registerIUserTargetingor pass explicitUserTargeting.
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
Related resources
- Product: https://onionswitch.com/
- Self-hosted: https://onionswitch.com/documentation/self-hosted/overview
- Support / FAQ: https://onionswitch.com/documentation/troubleshoot/faq
License
MIT — See LICENSE.
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.Caching.Memory (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.2)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.2)
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.2)
- System.IO.Hashing (>= 10.0.2)
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 |