Installation
dotnet add package OmniFlags.Sdk
Requirements: .NET 8+, ASP.NET Core (for the hosted service integration)
Setup
Register OmniFlags in Program.cs. The SDK key is the only required configuration — polling interval, CDN endpoints, and all other operational settings are managed server-side and delivered with the snapshot.
// Program.cs
builder.Services.AddOmniFlags(options =>
{
options.SdkKey = builder.Configuration["OmniFlags:SdkKey"]
?? throw new InvalidOperationException("OmniFlags:SdkKey is required.");
});
Store the SDK key in appsettings.json. Use environment-specific overrides (appsettings.Production.json) or secret management to keep production keys out of source control.
{
"OmniFlags": {
"SdkKey": "sk_live_..."
}
}
AddOmniFlags registers:
OmniFlagsClient as a singleton in the DI container
- A hosted service that fetches the initial snapshot at startup, before the application begins accepting traffic
By the time the first request arrives, the flag snapshot is loaded and OmniFlagsClient is ready to evaluate.
Evaluating flags
Inject OmniFlagsClient into services, controllers, or minimal API endpoints. The client is thread-safe and designed to be shared as a singleton.
In a service class
public class CheckoutService
{
private readonly OmniFlagsClient _flags;
public CheckoutService(OmniFlagsClient flags)
{
_flags = flags;
}
public OrderResult PlaceOrder(CheckoutRequest req)
{
var ctx = new EvaluationContext { CustomerId = req.CustomerId };
var chargeDeliveryFee = _flags.IsEnabled("checkout.charge-delivery-fee", ctx);
return new OrderResult
{
DeliveryFee = chargeDeliveryFee ? CalculateFee(req) : 0m,
};
}
}
In a minimal API endpoint
app.MapGet("/checkout", (string customerId, OmniFlagsClient flags) =>
{
var ctx = new EvaluationContext
{
CustomerId = long.TryParse(customerId, out var id) ? id : null,
};
var chargeDeliveryFee = flags.IsEnabled("checkout.charge-delivery-fee", ctx);
return Results.Ok(new { chargeDeliveryFee });
});
Client API
IsEnabled — boolean flags
bool IsEnabled(string flagKey, EvaluationContext? context = null, bool defaultValue = false)
Returns the flag value as a bool. The defaultValue is returned if the flag is not found, is disabled, or the client is not yet ready.
var chargeDeliveryFee = client.IsEnabled("checkout.charge-delivery-fee", ctx);
GetString — string flags
string GetString(string flagKey, string defaultValue, EvaluationContext? context = null)
var bannerColour = client.GetString("product-listing.promo-banner-colour", "blue", ctx);
GetNumber — number flags
double GetNumber(string flagKey, double defaultValue, EvaluationContext? context = null)
var maxCartItems = client.GetNumber("cart.max-items", 20, ctx);
Detail variants
Every typed method has a *Detail variant that returns the full EvaluationResult<T>. Use this when you need to inspect why a value was returned — useful for logging, analytics, or debugging evaluation logic.
bool IsEnabledDetail(string flagKey, bool defaultValue, EvaluationContext? context = null)
→ EvaluationResult<bool>
string GetStringDetail(string flagKey, string defaultValue, EvaluationContext? context = null)
→ EvaluationResult<string>
double GetNumberDetail(string flagKey, double defaultValue, EvaluationContext? context = null)
→ EvaluationResult<double>
var result = client.IsEnabledDetail("checkout.charge-delivery-fee", false, ctx);
logger.LogInformation(
"Flag {Key} → {Value} (reason: {Reason}, rule: {RuleId})",
"checkout.charge-delivery-fee",
result.Value,
result.Reason,
result.RuleId
);
| Field | Type | Description |
|---|
Value | T | The resolved flag value |
Variant | string? | The matched variant key, or null for boolean rollouts |
Reason | EvaluationReason | Why this value was returned — see reason codes |
RuleId | string? | The ID of the targeting rule that matched, if any |
ErrorCode | ErrorCode? | Set when Reason is Error |
Evaluation context
Construct an EvaluationContext with the user and session attributes available at the call site. All properties are optional — include only what is meaningful for your targeting rules.
var ctx = new EvaluationContext
{
CustomerId = 12345,
Country = "Nigeria",
City = "Lagos",
Platform = "api",
AppVersion = "3.1.0",
};
// Pass custom attributes for rule matching
ctx["plan"] = "enterprise";
ctx["tier"] = "gold";
| Property | Type | Notes |
|---|
CustomerId | long? | Primary bucketing key for rollout and traffic splits |
AgentId | long? | Secondary bucketing key; used when CustomerId is absent |
BusinessId | long? | Tertiary bucketing key |
BusinessBranchId | long? | |
Country | string? | Full country name (e.g. "Nigeria", "Ghana") |
City | string? | |
Platform | string? | web, ios, android, api, etc. |
AppVersion | string? | Semver string |
Additional key-value pairs set on the context dictionary are available for custom attribute matching in targeting rules, including dot-path traversal (e.g., ctx["user.plan"] = "enterprise").
Rollout and traffic splits require a bucketing key (CustomerId, AgentId, etc.) to produce a deterministic result. If no bucketing key is present, the SDK returns the flag’s default value with ErrorCode.MissingTargetingKey.
Startup behaviour
The hosted service registered by AddOmniFlags runs InitializeAsync during IHostedService.StartAsync, which is called by the ASP.NET Core runtime as part of the startup pipeline — before the application begins serving traffic. This means:
- Flags are always ready on the first request. No warm-up period, no stale defaults on startup.
- Startup failure does not block the application. If the CDN is unreachable at startup, the hosted service logs the error and the client falls back to an empty snapshot. The application starts, and evaluation returns caller-supplied defaults until the next successful poll.
Background polling continues throughout the application’s lifetime. The polling interval is set server-side and delivered in the snapshot.
Shutdown
OmniFlagsClient implements IAsyncDisposable. The DI container disposes the singleton as part of the host shutdown sequence. No manual teardown is required.