Skip to main content

.NET

Install the latest version​

GitHub | NuGet

Replace 1.4.0 with the latest version on NuGet.

dotnet add package Quonfig.Sdk --version 1.4.0

Requirements: net8.0 (current LTS) or any runtime compatible with netstandard2.0 (including .NET Framework 4.6.2+).

Companion packages​

The .NET SDK ships as four NuGet packages published lock-step from one tag. Add only what you need:

PackagePurpose
Quonfig.SdkCore SDK — evaluation, transport, SSE, telemetry, datadir, datafile.
Quonfig.Sdk.AspNetCoreDI + IHostedService + per-request ContextSet via HttpContext.
Quonfig.Sdk.Extensions.LoggingILoggerProvider filter for dynamic log levels via Microsoft.Extensions.Logging.
Quonfig.Sdk.SerilogSerilog LoggingLevelSwitch provider for dynamic log levels.

Initialize the client​

The core SDK exposes a single client class, Quonfig.Sdk.Quonfig, configured with a QuonfigOptions instance.

If you set QUONFIG_BACKEND_SDK_KEY in your environment, initialization is a one-liner:

using Quonfig.Sdk;

await using var client = new Quonfig.Sdk.Quonfig(new QuonfigOptions());
await client.InitAsync();
Why Quonfig.Sdk.Quonfig?

The client type Quonfig lives in the Quonfig.Sdk namespace, so the bare name Quonfig collides with the root Quonfig namespace (error CS0118: 'Quonfig' is a namespace but is used like a type). The examples below fully-qualify the constructor as new Quonfig.Sdk.Quonfig(...). If you prefer the short name, add a using alias: using QuonfigClient = Quonfig.Sdk.Quonfig;.

Or pass the SDK key explicitly:

await using var client = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
SdkKey = Environment.GetEnvironmentVariable("QUONFIG_BACKEND_SDK_KEY"),
});
await client.InitAsync();

Quonfig implements IAsyncDisposable. We recommend wiring it as a singleton in your DI container and letting the container call DisposeAsync() on shutdown to stop the SSE stream and send the last telemetry window. In ASP.NET Core the Quonfig.Sdk.AspNetCore package handles this for you.

Initialization is asynchronous​

InitAsync() runs the first config fetch (or datadir read) and completes once the initial envelope has been installed. After it returns, every typed getter (GetString, GetBool, etc.) is a synchronous in-memory read — no Async suffix, no Task allocation per call.

If initialization fails or times out, the OnInitFailure option decides what happens:

  • OnInitFailure.Throw (default) — InitAsync() throws QuonfigInitTimeoutException.
  • OnInitFailure.ReturnDefaults — InitAsync() returns; subsequent getter calls fall back to the caller's defaultValue until the background fetch eventually succeeds.
await using var client = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
SdkKey = "sdk-...",
InitTimeout = TimeSpan.FromSeconds(10),
OnInitFailure = OnInitFailure.ReturnDefaults,
});
await client.InitAsync();

Datadir mode (local development)​

To run the SDK against a local checkout of a Quonfig workspace directory (the configs/, feature-flags/, segments/, log-levels/ tree), use Datadir. Environment is required in datadir mode — set it on QuonfigOptions or via the QUONFIG_ENVIRONMENT env var.

await using var client = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
Datadir = "/path/to/your-workspace",
Environment = "development",
});
await client.InitAsync();

In datadir mode the SDK loads everything synchronously from disk; there is no background fetch and no SSE connection. Telemetry is still uploaded if an SdkKey is also set.

To pick up edits to the workspace without restarting the process, opt into auto-reload — see Auto-reload on file changes.

await using var client = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
Datadir = "/path/to/your-workspace",
Environment = "development",
DatadirAutoReload = true,
DatadirAutoReloadDebounce = TimeSpan.FromMilliseconds(200),
});
await client.InitAsync();

Datafile mode​

For build-time embedding (or tests), point Datafile at a single serialized envelope. The SDK loads it synchronously and makes no network calls.

await using var client = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
Datafile = "/path/to/envelope.json",
});
await client.InitAsync();

You can also pass a pre-parsed envelope via DatafileEnvelope — useful when you already have the JSON in memory and don't want to round-trip through the filesystem.

Domain and API URL configuration​

By default the SDK derives every service URL from a single domain (quonfig.com):

  • API: https://primary.quonfig.com, https://secondary.quonfig.com
  • SSE stream: https://stream.primary.quonfig.com, https://stream.secondary.quonfig.com
  • Telemetry: https://telemetry.quonfig.com

You can override the URL lists directly on QuonfigOptions:

await using var client = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
SdkKey = "sdk-...",
ApiUrls = new[] { "https://primary.example.com" },
StreamUrls = new[] { "https://stream.primary.example.com" },
TelemetryUrl = "https://telemetry.example.com",
});

For local development with the bundled Caddy reverse proxy (scripts/local-proxy/setup.sh in the monorepo), set QUONFIG_DOMAIN=quonfig-localhost in your environment — the SDK will route to primary.quonfig-localhost, stream.primary.quonfig-localhost, etc.

Feature Flags​

For boolean flags, use IsFeatureEnabled — it always returns a bool, never throws, and defaults to false when the key is missing:

if (client.IsFeatureEnabled("my.feature.name"))
{
// ...
}

A flag that doesn't exist yet evaluates to false, so you can safely add IsFeatureEnabled checks before the flag is created.

Feature flags don't have to return just true or false.

You can return any supported type using the typed getters described in Dynamic Config below.

string variant = client.GetString("my.string.feature.name", defaultValue: "control")!;

Context​

Feature flags become more powerful when you give the evaluator context about the current user, team, request, or host. The .NET SDK uses Quonfig.Sdk.ContextSet to bundle one or more named contexts (e.g. user, team, device).

using Quonfig.Sdk;

var ctx = new ContextSet
{
["user"] = new()
{
["key"] = "user-123",
["email"] = "alice@example.com",
},
["team"] = new()
{
["key"] = "team-abc",
["plan"] = "pro",
},
};

Implicit conversions on ContextValue let you assign primitive C# values directly — string, int, long, double, bool, and string[] all work without wrapping.

Properties are looked up in rules using dotted notation: user.email, team.plan. The magic property quonfig.current-time (also prefab.current-time and reforge.current-time, kept for compatibility) resolves to the current wall-clock time in milliseconds since the epoch — useful for time-windowed rollouts.

Global Context​

A global context is merged into every evaluation. Use it for values that don't change at runtime (application name, region, host).

var global = new ContextSet
{
["application"] = new()
{
["key"] = "my-api",
["region"] = Environment.GetEnvironmentVariable("REGION") ?? "us-east",
},
};

await using var client = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
SdkKey = "sdk-...",
GlobalContext = global,
});
await client.InitAsync();

Global context is the least specific layer and is overridden by any per-call context that supplies the same key.

Bound Context (per-request)​

For web servers, bind context for the lifetime of a request via WithContext(...). The returned IBoundQuonfig mirrors every typed getter, with the bound context pre-applied — no need to thread ContextSet through every call site.

var requestCtx = new ContextSet
{
["user"] = new()
{
["key"] = currentUser.Id,
["email"] = currentUser.Email,
},
};

IBoundQuonfig bound = client.WithContext(requestCtx);

if (bound.IsFeatureEnabled("my.feature.name"))
{
// ...
}

In ASP.NET Core, Quonfig.Sdk.AspNetCore builds the bound client per request automatically — see ASP.NET Core integration below, or the per-request context how-to.

Just-in-time Context​

You can also pass a ContextSet directly to any getter call. It merges with the global (and bound) context; per-call values win on key collision.

var jitCtx = new ContextSet
{
["device"] = new() { ["mobile"] = true },
};

bool enabled = client.IsFeatureEnabled("my.feature.name", jitCtx);

Dynamic Config​

Config values are read with typed getters that take a key, an optional ContextSet, and a default. Getters never throw on missing or wrong-type config when you pass a default — they fall back to the supplied value.

string  backend     = client.GetString("backend.url", defaultValue: "https://api.example.com")!;
bool newCheckout = client.GetBool("checkout.v2", defaultValue: false) ?? false;
long maxJobs = client.GetLong("max-jobs-per-second", defaultValue: 10L) ?? 10L;
double sampleRate = client.GetDouble("trace.sample-rate", defaultValue: 0.1) ?? 0.1;
var regions = client.GetStringList("regions", defaultValue: new[] { "us-east" });
TimeSpan cacheTtl = client.GetDuration("cache.ttl", defaultValue: TimeSpan.FromSeconds(30)) ?? TimeSpan.FromSeconds(30);
object? slackConfig = client.GetJson("slack.bot.config", defaultValue: new Dictionary<string, object>());
32-bit GetInt narrows from a 64-bit wire format

Quonfig stores int configs as 64-bit values everywhere. GetInt narrows to int; if you need the full range, use GetLong. The narrower getter is provided for convenience when you know the value fits in 32 bits.

OnNoDefault policy​

If you call a plain typed getter (GetString, GetInt, GetLong, GetBool, GetDouble, GetStringList, GetJson, GetDuration) without a defaultValue and the config's value cannot be resolved (the cases in the table below), the SDK consults the OnNoDefault option:

  • OnNoDefault.Throw (default) — throws the QuonfigException subtype that says what went wrong (see the table below).
  • OnNoDefault.Warn — logs a warning and returns null.
  • OnNoDefault.Ignore — returns null.

Under OnNoDefault.Throw a plain typed getter with no defaultValue throws in these cases:

CaseException
The key does not exist in the active environment.QuonfigKeyNotFoundException
The value comes from an environment variable that is not set.QuonfigEnvVarNotSetException
The environment variable is set but cannot be converted to the config's type.QuonfigCoercionException
A confidential value cannot be decrypted (for example, a wrong or missing key).QuonfigDecryptionException
The stored value cannot be converted by the getter, for example GetInt on a value above int.MaxValue.QuonfigCoercionException

A config whose value type does not match the getter, such as GetInt on a string config, is not one of these cases: the getter returns your default (or null) in every OnNoDefault mode, without throwing or logging. Pass a defaultValue to get your default instead of an exception in every case above. Apart from a missing key, each case also logs one warning per config key, whatever the OnNoDefault setting; the warning never includes the raw value.

In Quonfig.Sdk 1.4.0 and earlier, only a missing key threw; the other cases returned null without a log.

The GetXDetails getters (see Evaluation details) never throw, whatever the OnNoDefault setting. In every case above, and for a value of a different type, they return your default (or null) with Reason.Error and an ErrorCode: FlagNotFound for a missing key, TypeMismatch for a value the getter cannot convert or a value of a different type, and General for an environment-variable or decryption failure.

IsFeatureEnabled always returns bool and bypasses OnNoDefault — it defaults to false on missing. ShouldLog always returns bool and walks the dotted-key hierarchy before falling back to true.

Each getter also accepts a context-bearing overload:

var ctx = new ContextSet { ["user"] = new() { ["plan"] = "pro" } };
long maxJobs = client.GetLong("max-jobs-per-second", contexts: ctx, defaultValue: 10L) ?? 10L;

Evaluation details (reason, variant, errors)​

Each typed getter has a GetXDetails(...) variant that returns an EvaluationDetails<T> instead of just the value. Use it when you need the resolution Reason, the synthetic OpenFeature Variant identifier, the matching rule index, or the error code on failure.

EvaluationDetails<string?> details = client.GetStringDetails(
"backend.url",
contexts: ctx,
defaultValue: "https://api.example.com");

if (details.Reason == Reason.Error)
{
logger.LogWarning(
"backend.url evaluation failed: {Message} ({Code})",
details.ErrorMessage, details.ErrorCode);
}

EvaluationDetails<T> carries:

FieldDescription
ValueThe typed value (or your default on Default / Error).
ReasonStatic, TargetingMatch, Split, Default, Error, or Unknown (see note on Split below).
VariantOpenFeature-style identifier — "static", "targeting:<n>", "split:<n>", or "default".
VariantIndexWeighted-bucket index — reserved and not yet wired, so always null. Read the bucket from Metadata["weightedValueIndex"] (or parse Variant).
ErrorCodeFlagNotFound, TypeMismatch, or General on Error; null otherwise.
ErrorMessageCompanion to ErrorCode.
MetadataconfigId, configKey, configType, optional ruleIndex (on TargetingMatch / Split) and weightedValueIndex (on Split), environment, and hashPropertyMissing (true only when a weighted rollout's hash property was missing from the context, so an empty value was hashed; since 1.4.0).
Weighted splits report Split, but VariantIndex is still null

Weighted-value configs resolve to the correct value and report Reason.Split with Variant == "split:<n>"; the chosen bucket is exposed as Metadata["weightedValueIndex"]. The dedicated VariantIndex field is not wired up yet, so it stays null — read the bucket index from Metadata (or parse it out of Variant) until a later release populates it.

ASP.NET Core integration​

The Quonfig.Sdk.AspNetCore companion package wires Quonfig into the ASP.NET Core host: it registers the singleton, runs InitAsync via IHostedService, and (optionally) binds per-request ContextSet from HttpContext so controllers can inject IBoundQuonfig directly.

dotnet add package Quonfig.Sdk.AspNetCore --version 1.4.0

Replace 1.4.0 with the latest version on NuGet. The companion packages ship lock-step with Quonfig.Sdk from the same tag, so keep the versions aligned.

// Program.cs
using Quonfig.Sdk;
using Quonfig.Sdk.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddQuonfig(opts =>
{
opts.SdkKey = builder.Configuration["Quonfig:SdkKey"];
// In delivery (SDK-key) mode the active environment comes from the SDK key —
// an explicit Environment pin is ignored (and logs a warning at init).
});

var app = builder.Build();

app.UseQuonfigContext((http, ctx) =>
{
if (http.User.Identity?.IsAuthenticated == true)
{
ctx["user"] = new()
{
["key"] = http.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "",
["email"] = http.User.FindFirstValue(ClaimTypes.Email) ?? "",
};
}
});

// Minimal API: inject the bound client and the request context is applied for you.
app.MapGet("/beta", (IBoundQuonfig q) =>
q.IsFeatureEnabled("beta.dashboard") ? "yes" : "no");

app.Run();

AddQuonfig registers a Quonfig singleton behind the IQuonfig interface (via TryAddSingleton<IQuonfig>), the request-scoped IBoundQuonfig, and an IHostedService that runs InitAsync() during application startup and CloseAsync() on shutdown. Inject IQuonfig (not the concrete Quonfig type — it is not registered as its own service). UseQuonfigContext registers middleware that builds the per-request ContextSet from HttpContext.

See the per-request context binding how-to for a full walkthrough.

Dynamic Log Levels​

Log levels in Quonfig are stored as log_level configs. The SDK consults them via ShouldLog(...), so changes pushed to Quonfig take effect immediately over SSE — no redeploy or polling.

ShouldLog(loggerPath, desired [, contexts]) returns true when a record at desired should be emitted for loggerPath. It always injects the logger path into the evaluation context as quonfig-sdk-logging.key, and uses one of two lookup strategies:

  • Single-config dispatch — when you set LoggerKey = "..." on QuonfigOptions, that one config is evaluated with quonfig-sdk-logging.key=<loggerPath> so a single config drives per-logger rules. This is the recommended pattern.
  • Per-logger lookup — when LoggerKey is unset, the SDK looks up a config keyed by loggerPath itself, then walks up dotted parents (Acme.Web.AuthService → Acme.Web → Acme → "") until one matches.

If no log-level config is found at any level, ShouldLog returns true — the SDK never silently swallows logs.

Microsoft.Extensions.Logging integration​

The Quonfig.Sdk.Extensions.Logging package wires ShouldLog into the BCL logging pipeline by wrapping the providers already registered on the ILoggingBuilder:

dotnet add package Quonfig.Sdk.Extensions.Logging --version 1.4.0

Replace 1.4.0 with the latest version on NuGet. Keep it aligned with the core Quonfig.Sdk version — the packages ship lock-step from one tag.

AddQuonfigFilter(quonfig) takes the client instance and must be called last in the logging setup — it snapshots and wraps every ILoggerProvider registered up to that point. Because it needs the instance at logging-config time, construct the client up front and register the same instance with DI:

using Quonfig.Sdk;
using Quonfig.Sdk.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);

// Construct the client up front so the one instance drives both the logging
// filter and DI.
var quonfig = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
SdkKey = builder.Configuration["Quonfig:SdkKey"],
LoggerKey = "log-level.my-app",
});
builder.Services.AddSingleton<IQuonfig>(quonfig);

// Add LAST, after the default console/debug providers are wired.
builder.Logging.AddQuonfigFilter(quonfig);

Every ILogger<T> call site is then automatically gated by Quonfig: the logger category name is used as loggerPath, the requested level becomes desired, and the SDK answers with the current ruleset. When you also use Quonfig.Sdk.AspNetCore, register the singleton as above and let AddQuonfig(...) mirror the same options — that way the IHostedService still drives InitAsync/CloseAsync on the instance you handed to the filter.

Serilog integration​

For Serilog, use Quonfig.Sdk.Serilog. The QuonfigLoggingLevelSwitchProvider manages a set of Serilog LoggingLevelSwitch instances keyed by source context and re-evaluates them whenever the config envelope changes (it subscribes to the SDK's OnConfigChange, which fires after every successful install).

dotnet add package Quonfig.Sdk.Serilog --version 1.4.0

Replace 1.4.0 with the latest version on NuGet. Keep it aligned with the core Quonfig.Sdk version — the packages ship lock-step from one tag.

The provider resolves levels via IQuonfig.GetLogLevel(...), so the config key is configured by LoggerKey on the client options — you don't pass a key to the provider. GetSwitch(category) returns (and caches) the switch for a source context; pass an empty string for the root switch used by MinimumLevel.ControlledBy:

using Serilog;
using Serilog.Events;
using Quonfig.Sdk.Serilog;

// `client` is a Quonfig instance whose options set LoggerKey = "log-level.my-app".
var levelSwitches = new QuonfigLoggingLevelSwitchProvider(
client,
defaultLevel: LogEventLevel.Information);

Log.Logger = new LoggerConfiguration()
.MinimumLevel.ControlledBy(levelSwitches.GetSwitch(string.Empty)) // root switch
.WriteTo.Console()
.CreateLogger();

To control a specific source context, request its switch with levelSwitches.GetSwitch("Acme.Web.Auth") and apply it with Serilog's MinimumLevel.Override. Every switch the provider hands out is refreshed automatically when the Quonfig config changes — no enricher or per-event hook required. Call levelSwitches.Dispose() on shutdown to unsubscribe from OnConfigChange.

Rule example​

Create a log_level config keyed log-level.my-app and target individual loggers via quonfig-sdk-logging.key:

# Default to INFO for every logger in this app
default: INFO

rules:
# Bump a subsystem to DEBUG
- criteria:
quonfig-sdk-logging.key:
starts-with: "Acme.Web.Auth"
value: DEBUG

# Silence a chatty third-party package
- criteria:
quonfig-sdk-logging.key:
starts-with: "Microsoft.AspNetCore.HostFiltering"
value: ERROR

# Turn DEBUG on for one developer, everywhere
- criteria:
user.email: "developer@example.com"
value: DEBUG

Because the evaluator sees your full context — global, bound, and the injected logger key — you can combine logger rules with user, environment, or request context to crank verbosity up for one user, one staging deploy, or one bad request, without touching anyone else.

Manual gating​

If you'd rather gate a single call site by hand instead of installing the filter, call ShouldLog directly. The LogLevel enum lives in Quonfig.Sdk. It is Quonfig's own level set, shared with the other Quonfig SDKs, ordered most-severe-first: Fatal, Error, Warn, Info, Debug, Trace. It is not Microsoft.Extensions.Logging.LogLevel, so qualify it as Quonfig.Sdk.LogLevel if both namespaces are imported.

using Quonfig.Sdk;

if (client.ShouldLog("Acme.Web.AuthService", LogLevel.Debug))
{
logger.LogDebug("expensive thing happened: {Description}", expensiveDescription());
}

Health primitives​

The client exposes two diagnostic surfaces useful for dashboards and structured logging:

DateTimeOffset? lastRefresh = client.LastSuccessfulRefresh;
ConnectionState state = client.ConnectionState;
// Initializing | Connected | Disconnected | FallingBack

client.OnConnectionStateChange += newState =>
logger.LogInformation("Quonfig SSE state -> {State}", newState);
Do not use these as a Kubernetes liveness probe

The SDK is designed so that a transient delivery outage never affects your application: in-memory evaluation continues against the last-good envelope, and the Layer 2 fallback poller takes over if SSE stays down. Using ConnectionState != Connected as a liveness signal would restart healthy pods during a delivery hiccup — the opposite of what you want.

Use these primitives for telemetry, dashboards, and alerting on extended outages instead.

Telemetry​

By default Quonfig uploads telemetry that powers the dashboard's evaluation counts, context-shape detection, and example-context capture. Tune or disable via QuonfigOptions:

NameDescriptionDefault
CollectEvaluationSummariesSend aggregate counts of config/flag evaluation results back to Quonfig.true
ContextUploadModeHow named-context data is reported. One of None, ShapesOnly (names + types), or PeriodicExample (shapes plus example contexts with values).PeriodicExample
ContextUploadMode default changed in 1.3.0

Before Quonfig.Sdk 1.3.0 the default was ShapesOnly, which uploads only context field names and types. The default is now PeriodicExample, matching every other Quonfig SDK. It additionally uploads example contexts: for each distinct context key (or trackingId), at most once per hour, the full evaluation context of that evaluation, meaning every named context (user, team, ...) including the GlobalContext, with all property values (for example user.email, user.name, team.plan). Contexts with neither a key nor a trackingId are never sampled. Example contexts power the dashboard's context drill-down and rule-editor autocomplete; see What gets saved.

To keep the old behavior, set ContextUploadMode = ContextUploadMode.ShapesOnly (names and types, no values), or ContextUploadMode.None to send no context data at all.

await using var client = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
SdkKey = "sdk-...",
CollectEvaluationSummaries = true,
ContextUploadMode = ContextUploadMode.ShapesOnly, // opt out of example contexts
});

To turn telemetry off entirely, set both fields:

new QuonfigOptions
{
SdkKey = "sdk-...",
CollectEvaluationSummaries = false,
ContextUploadMode = ContextUploadMode.None,
};

client.DisposeAsync() (or CloseAsync()) stops the SSE client, fallback poller, datadir watcher, and telemetry reporter. It sends the current telemetry window once with a 5s deadline and never blocks shutdown on a slow telemetry endpoint.

Delivery options​

How the SDK delivers telemetry, and what it does when the endpoint is slow or down, is the same in every SDK and is explained once on the Telemetry page. The .NET property names on QuonfigOptions and their defaults (Quonfig.Sdk 1.3.0+):

PropertyDefault
TelemetryFlushIntervalTimeSpan.FromSeconds(60)
TelemetryTimeoutTimeSpan.FromSeconds(15)
TelemetryConnectTimeoutTimeSpan.FromSeconds(5)
TelemetryMaxRetainedBatches5
TelemetryMaxRetainedBytes2097152 (2MB)
TelemetryMaxRetainedAgeTimeSpan.FromMinutes(5)
TelemetryMaxEvaluationSummaries10000
TelemetryMaxContextShapeFields10000
TelemetryMaxExampleContexts10000

Non-positive values fall back to the default. TelemetryConnectTimeout applies on net8.0 when the SDK builds its own HTTP handler; on netstandard2.0, or with an injected HttpMessageHandler, TelemetryTimeout covers connect too. TelemetryInitialDelay and TelemetryMaxInterval are kept for source compatibility but no longer used.

Telemetry logs through QuonfigOptions.Logger. The default is a no-op logger, so pass an ILogger to see telemetry drops (Warning), recovery (Information) and auth errors.

Changes in 1.3.0: besides the ContextUploadMode default above, a timed-out telemetry POST no longer stops telemetry for the rest of the process; the timeout went from 30s to 15s; a failed POST logs at Debug instead of Warning every time.

Testing​

Quonfig is a regular class with no static state. The cleanest way to unit-test code that reads configs is to point a Quonfig instance at a small datadir of test fixtures:

await using var testClient = new Quonfig.Sdk.Quonfig(new QuonfigOptions
{
Datadir = "TestData/quonfig-fixtures",
Environment = "test",
CollectEvaluationSummaries = false,
ContextUploadMode = ContextUploadMode.None,
});
await testClient.InitAsync();

For finer-grained tests, mock IQuonfig (or IBoundQuonfig) with your favorite mocking library and stub the typed getters directly — both interfaces expose the full public surface.

Troubleshooting​

QuonfigInitTimeoutException on startup​

InitAsync waits up to InitTimeout (default 10s) for the first envelope. The most common causes:

  • Network or DNS issue reaching primary.quonfig.com / secondary.quonfig.com. Verify ApiUrls and proxy settings.
  • A wrong or revoked SDK key. The SDK uses the SDK key as the HTTP Basic-auth password; a 401 surfaces as init failure.
  • A custom HttpMessageHandler is dropping the request. Confirm the handler forwards to a real network sink.

To soft-fail instead of throwing, set OnInitFailure = OnInitFailure.ReturnDefaults. Getters then serve caller-supplied defaults until the background fetch eventually succeeds.

QuonfigKeyNotFoundException on a typed getter​

The key isn't in the active envelope and you didn't pass a defaultValue while OnNoDefault = Throw. Either pass a default, switch OnNoDefault to Warn / Ignore, or check client.Keys() to confirm the key was loaded for the current Environment.

QuonfigEnvVarNotSetException, QuonfigCoercionException, or QuonfigDecryptionException on a typed getter​

The key exists but its value could not be resolved, and you didn't pass a defaultValue while OnNoDefault = Throw. The exception type and message give the cause: an unset environment variable, a value that cannot be converted to the getter's type, or a failed decryption. Fix the value or the environment, or pass a default. See OnNoDefault policy.

Datadir mode loads zero keys​

Datadir mode requires Environment to be set (on QuonfigOptions or via QUONFIG_ENVIRONMENT). The SDK reads the entire workspace tree but only the matching environment's rules are surfaced. Verify the workspace's quonfig.json lists your environment and that the JSON files live in the expected configs/, feature-flags/, segments/, log-levels/ directories.

SSE stays disconnected​

Check client.ConnectionState. If you're seeing FallingBack for long stretches, the Layer 2 poller is keeping the cache fresh — your app is still serving correct values, but real-time updates are not flowing. Inspect logs for SSE reconnect attempts and verify stream.primary.quonfig.com is reachable from the deploy environment.

OnConnectionStateChange doesn't fire​

Subscribers run inline. If your handler throws, the SDK logs and swallows the exception — verify you're not throwing on the first invocation. Also confirm you subscribed before calling InitAsync(); the first transition (Initializing → Connected) happens during init.

Reference​

QuonfigOptions​

using Quonfig.Sdk;

var options = new QuonfigOptions
{
SdkKey = Environment.GetEnvironmentVariable("QUONFIG_BACKEND_SDK_KEY"),
GlobalContext = global,
InitTimeout = TimeSpan.FromSeconds(10),
OnInitFailure = OnInitFailure.Throw,
OnNoDefault = OnNoDefault.Throw,
LoggerKey = "log-level.my-app",
CollectEvaluationSummaries = true,
ContextUploadMode = ContextUploadMode.PeriodicExample,
};

Option Definitions​

NameDescriptionDefault
SdkKeyYour Quonfig SDK key. Required for HTTP+SSE mode. Falls back to QUONFIG_BACKEND_SDK_KEY env var.(env var)
ApiUrlsOrdered API base URLs (primary first, then failover).[primary.quonfig.com, secondary.quonfig.com]
StreamUrlsOrdered SSE stream base URLs.[stream.primary.quonfig.com, stream.secondary.…]
TelemetryUrlTelemetry endpoint.https://telemetry.quonfig.com
EnvironmentEnvironment to evaluate against. Required in datadir mode; ignored in delivery (SDK-key) mode, where the key selects it (a set value logs a warning). Falls back to QUONFIG_ENVIRONMENT when unset.(env var)
DatadirPath to a Quonfig workspace directory. Switches the client to datadir mode (no HTTP fetch, no SSE).null
DatafilePath to a serialized envelope file. Switches the client to datafile mode.null
DatafileEnvelopePre-parsed envelope instance (mutually exclusive with Datafile).null
DatadirAutoReloadOpt-in: watch Datadir for file changes and reload atomically.false
DatadirAutoReloadDebounceDebounce window when DatadirAutoReload is on.200ms
InitTimeoutHow long the initial fetch / load may take before OnInitFailure applies.10s
ConfigFetchTimeoutPer-URL deadline for one config-fetch attempt (initial fetch, fallback poller, refresh). A hung primary aborts after this and the next URL is tried within the remaining InitTimeout. Never applies to the SSE stream.3s
ConfigFetchHedgeDelayHow long the hedged fetch waits on the primary before also firing the secondary in parallel (the primary is not cancelled). Must be less than ConfigFetchHedgeAbort.2s
ConfigFetchHedgeAbortPer-leg hard-abort deadline on the hedged fetch path. Must be less than InitTimeout (a warning is logged at construction otherwise).6s
OnInitFailureThrow or ReturnDefaults when init exceeds InitTimeout.Throw
OnNoDefaultThrow, Warn, or Ignore when a getter has no value and no defaultValue.Throw
GlobalContextA ContextSet merged into every evaluation as the base layer.null
FallbackPollEnabledMaster switch for the Layer 2 fallback poller.true
FallbackPollIntervalCadence between fallback fetches once engaged.60s
FallbackPollThresholdSSE-down duration before Layer 2 engages.120s
SseReadTimeoutLayer 1 SSE read watchdog. Pass TimeSpan.Zero to disable.90s
LoggerKeyConfig key consulted by ShouldLog(...). When set, enables single-config dispatch via injected quonfig-sdk-logging.key.null
CollectEvaluationSummariesSend aggregate evaluation counts to Quonfig.true
ContextUploadModeNone, ShapesOnly, or PeriodicExample. See Telemetry.PeriodicExample
TelemetrySenderOptional ITelemetrySender used instead of the built-in HTTP sender (tests / DI). Telemetry opt-outs still apply; a sender that throws is treated as a retryable failure.null
OnConfigChangeOptional Action fired after every config install (initial load and each SSE / fallback / datadir refresh). Same as subscribing to the client's OnConfigChange event before the first load. Must not throw.null
EnableQuonfigUserContextAuto-inject the dev-only quonfig-user.email context from the qfg login tokens file. null falls through to the QUONFIG_DEV_CONTEXT env var, then on; inert when no tokens file exists (e.g. production). Set false to force off.null (on)
LoggerOptional ILogger. Defaults to a no-op logger.no-op
HttpMessageHandlerOptional HttpMessageHandler for tests / DI. Ownership stays with the caller.null
EnvLookupOptional env-var lookup override (testability).Environment.GetEnvironmentVariable