Maxlona.FeatureFlags.Offline
1.4.2
dotnet add package Maxlona.FeatureFlags.Offline --version 1.4.2
NuGet\Install-Package Maxlona.FeatureFlags.Offline -Version 1.4.2
<PackageReference Include="Maxlona.FeatureFlags.Offline" Version="1.4.2" />
<PackageVersion Include="Maxlona.FeatureFlags.Offline" Version="1.4.2" />
<PackageReference Include="Maxlona.FeatureFlags.Offline" />
paket add Maxlona.FeatureFlags.Offline --version 1.4.2
#r "nuget: Maxlona.FeatureFlags.Offline, 1.4.2"
#:package Maxlona.FeatureFlags.Offline@1.4.2
#addin nuget:?package=Maxlona.FeatureFlags.Offline&version=1.4.2
#tool nuget:?package=Maxlona.FeatureFlags.Offline&version=1.4.2
Maxlona.FeatureFlags.Offline for .NET
The official .NET SDK for Maxlona. It covers the complete public feature-flag API:
- Evaluate boolean, string, number, and JSON variants with targeting context.
- Request decision explanations and read additional evaluation metadata.
- List, get, create, edit, archive, and delete feature flags.
- Configure environment/client state, percentage rollouts, targeting rules, dependencies, variants, and enrollment cutoffs.
- Use application services, structured exceptions, cancellation tokens, and .NET dependency injection.
- Opt into encrypted local evaluation with background SignalR synchronization and safe-off cold starts.
- Survive an unreliable network: Polly retries with exponential backoff and jitter, a per-attempt timeout, and plain-language firewall/proxy/DNS diagnostics.
- Keep a local Serilog log of every retry and failure, on by default.
The package targets .NET 8 and is published on NuGet.org as Maxlona.FeatureFlags.Offline.
Full integration documentation is available in the Maxlona SDK Wiki.
Package
Current SDK release documented here: Maxlona.FeatureFlags.Offline 1.1.0
Licensed under the Maxlona Proprietary SDK License, included as LICENSE.txt
in the package. Copyright (c) 2026 Maxlona. All rights reserved. This SDK is not
open source. The license permits integration with Maxlona and distribution of
unmodified SDK binaries as part of integrating applications; see the included
license for the full terms. Third-party dependencies retain their own licenses.
| Package information | Value |
|---|---|
| Package ID | Maxlona.FeatureFlags.Offline |
| Version | 1.1.0 |
| NuGet.org | https://www.nuget.org/packages/Maxlona.FeatureFlags.Offline/1.1.0 |
| Published by | Maxlona |
| Target framework | net8.0 — runs on .NET 8, 9, and 10 |
| Documentation | https://maxlona.com/wiki/sdk |
Dependencies, all pulled in automatically:
| Package | Minimum version |
|---|---|
Microsoft.Extensions.Http |
8.0.1 |
Microsoft.Extensions.Hosting.Abstractions |
8.0.1 |
Microsoft.AspNetCore.SignalR.Client |
8.0.10 |
Polly.Core |
8.5.2 |
Serilog |
4.2.0 |
Serilog.Sinks.File |
6.0.0 |
Install
dotnet add package Maxlona.FeatureFlags.Offline
A newly published version takes a few minutes to be indexed by NuGet.org. If a version that was just released is not
found yet, wait and retry, or run dotnet restore --no-cache to bypass a stale local index. Versions already
published are unaffected.
Or pin the version:
dotnet add package Maxlona.FeatureFlags.Offline --version 1.1.0
<PackageReference Include="Maxlona.FeatureFlags.Offline" Version="1.1.0" />
No custom feed is required: NuGet.org is a default source in a standard NuGet configuration. If your build uses a
nuget.config that clears the default sources, restore nuget.org there or point at the internal mirror that
proxies it.
Minimal setup
Register the service with a key and environment, then inject IFeatureFlags into your application:
using Maxlona.FeatureFlags.Offline;
using Maxlona.FeatureFlags.Offline.Models;
builder.Services.AddMaxlonaFeatureFlags(
builder.Configuration["Maxlona:ManagementKey"]!, "production");
public sealed class Checkout(IFeatureFlags flags)
{
public Task<bool> UseNewCheckout(string userId) =>
flags.IsEnabledAsync("checkout-redesign",
new EvaluationContext { UserId = userId }, defaultValue: false);
}
For administration, register AddMaxlonaFeatureFlagManagement(managementKey) and inject
IFeatureFlagManagement. The SDK handles endpoint selection, authentication, transport, retries, and logging.
An environment is still required for evaluation because it selects which flag configuration to use.
Local evaluation (opt in)
For high-volume or outage-sensitive services, local mode downloads the configuration for one flag/environment-scoped streaming key, encrypts each cache section with AES-256-GCM, and evaluates without network calls on the request path. SignalR only invalidates the cache; the SDK then downloads the sections whose content versions changed.
Local evaluation is not enabled by default. Use AddMaxlonaLocalFeatureFlags to enable it. Existing
AddMaxlonaFeatureFlags registrations retain their remote HTTP behavior, so upgrading the package does not silently
change production evaluation semantics.
string? cacheDirectory = builder.Configuration["Maxlona:CacheDirectory"];
builder.Services.AddMaxlonaLocalFeatureFlags(options =>
{
options.Stage = "production";
options.StreamingKey = builder.Configuration["Maxlona:StreamingKey"]!;
// Base64 for exactly 32 random bytes. Store separately from the streaming key.
options.CacheEncryptionKey = builder.Configuration["Maxlona:CacheEncryptionKey"]!;
if (!string.IsNullOrWhiteSpace(cacheDirectory))
options.CacheDirectory = cacheDirectory; // otherwise the SDK uses its platform default
});
The host must start registered hosted services. ASP.NET Core and a normal .NET Generic Host do this automatically.
For a console application, build and run an IHost; resolving LocalFeatureFlagClient from a service collection
without starting its host will load an existing disk cache but will not run initial synchronization or SignalR.
Where the local-evaluation keys come from
StreamingKey: In the Maxlona console, open Develop → Streaming, choose the flag and environment, and generate a Streaming Key. Copy the full key when it is displayed; only its non-secret identifier is shown afterward. One key is scoped to exactly one flag and environment. If the key is lost, rotate it and store the replacement in your application's secret manager.CacheEncryptionKey: This key is not issued by Maxlona. Generate 32 cryptographically random bytes once, encode them as base64, and store the result in your deployment secret manager. For example:using System.Security.Cryptography; string cacheEncryptionKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
Do not commit either key, derive the cache key from the Streaming Key, or generate a new cache key at every
startup. Every instance that shares a cache must receive the same cache key. Rotating the Streaming Key does not
require rotating the cache key. If no verified snapshot is available,
the application still starts and the configured flag safely evaluates to off; the background service keeps
trying to synchronize. Inject LocalFeatureFlagClient when synchronization diagnostics are needed.
app.MapGet("/health/maxlona", (LocalFeatureFlagClient client) =>
Results.Ok(client.Diagnostics));
Diagnostics report readiness, the scoped flag, active revision, snapshot generation time, SignalR connection
state, and the last synchronization error. Readiness becomes true only after a complete authenticated snapshot
is available. Local mode is enabled only by AddMaxlonaLocalFeatureFlags; the existing
AddMaxlonaFeatureFlags registration continues using remote HTTP evaluation.
How synchronization and notifications work
One local client represents the single flag and environment encoded by its Streaming Key. The configured Stage
must match that key's environment (matching is case-insensitive). Use a separate host/client registration and key
for every additional flag/environment scope.
At startup, the SDK follows this sequence:
- It decrypts and validates the last complete cache before making a network request. A valid snapshot becomes immediately available for evaluation, including during a Maxlona outage.
- It requests the current manifest with the cached revision as an
If-None-Matchvalue. Unchanged configuration returns304 Not Modified, so no configuration sections are downloaded. - It downloads only sections whose content versions differ, validates their scope and version, writes each section atomically, and activates the new immutable snapshot only after every required section is valid.
- It connects to
/hubs/flagsusing the Streaming Key and listens forflagChanged. SignalR is an invalidation signal; evaluation data is always fetched from the authenticated configuration endpoints, never trusted from a notification payload. - After a notification or reconnection, it repeats the conditional manifest synchronization. Duplicate or rapid notifications are serialized, and already-current section versions are not downloaded again.
Maxlona publishes affected-flag notifications when any decision input changes, including:
- The flag is enabled, disabled, archived, restored, scheduled, expired, edited, promoted, or unpromoted.
- Environment configuration, client configuration, targeting rules, variants, rollout, or enrollment changes.
- A referenced segment or applicable kill switch changes.
- Subscription/evaluation status changes.
- Automation changes the flag or its configuration.
- A parent dependency changes. Maxlona fans this out to every direct and transitive child because the child's evaluation result may have changed even though its own record did not.
Cache invalidation, SignalR publication, and dependency fan-out happen on the server in the background. They do not depend on an administrator keeping the management page open. Change publication is recorded transactionally and retried; if a live message is missed, the SDK's reconnect manifest check converges to the current revision.
SignalR must be enabled for realtime updates. If it is intentionally disabled, the local client continues serving its last verified snapshot and reconciles at its next startup or connection attempt; version 1.0.0 does not perform periodic manifest polling while a connection is intentionally unavailable.
What is cached
The snapshot is split into independently versioned files so a one-rule change does not rewrite the entire cache:
- Flag metadata, lifecycle state, and variants.
- Environment/client configurations, targeting rules, rollout, and enrollment settings.
- Referenced segments.
- The complete dependency closure required to evaluate the scoped flag.
- Applicable kill-switch state.
- Subscription/evaluation status.
- Sticky rollout enrollment in its own encrypted section.
Every file uses AES-256-GCM with a new random nonce. Its scope, section name, and version are authenticated as associated data. A modified, truncated, wrong-key, wrong-version, or cross-scope file fails authentication and is ignored. Section writes use a flushed temporary file followed by atomic replacement. The manifest is activated only after a complete snapshot can be reconstructed, so a failed download or partial write cannot replace the last valid in-memory revision.
By default, files are stored under:
%LOCALAPPDATA%\Maxlona\FeatureFlags\<streaming-key-scope-hash>\
On Windows this normally resolves to
C:\Users\<user>\AppData\Local\Maxlona\FeatureFlags\<16-character-hash>\.
Set CacheDirectory explicitly for containers, shared hosts, or any deployment where the platform-local directory
is ephemeral. The child scope directory is derived from the Streaming Key, but the key itself is never written to
disk. Do not edit or copy cache files between scopes.
Request-path and failure behavior
Local evaluation reads one immutable in-memory snapshot. It performs no HTTP, SignalR, Redis, disk I/O, or synchronization-lock wait on the request path, and it does not send exposure events.
If Maxlona becomes unreachable after a successful synchronization, the client continues using the last complete verified revision indefinitely while the background service retries with bounded exponential backoff and jitter. If the flag has never synchronized, the key does not match the cached flag, or no valid cache can be decrypted:
IsEnabledAsyncreturnsfalse.EvaluateAsync(..., explain: true)returns variantfalse, keyoff, and reasonconfiguration_unavailable.- Typed evaluation returns the off value represented by the safe-off result. Callers should still provide and apply an application default when deserializing configuration-shaped values.
In version 1.0.0 a missing or disabled configuration never returns an on default/first variant, and environment
configuration matching treats values such as DEV and dev as the same environment.
Evaluate flags
Use a Management Key with Evaluate permission. Scope it to the application, and optionally to one environment. Keep trusted-server keys out of browser-delivered code.
using Maxlona.FeatureFlags.Offline;
using Maxlona.FeatureFlags.Offline.Models;
builder.Services.AddMaxlonaFeatureFlags(options =>
{
options.ManagementKey = builder.Configuration["Maxlona:ManagementKey"]!;
options.Stage = "production";
options.ClientId = "checkout-api"; // optional
});
// Resolve IFeatureFlags through DI.
var result = await flags.EvaluateAsync(
"checkout-redesign",
new EvaluationContext
{
UserId = user.Id,
Attributes = new Dictionary<string, object?>
{
["plan"] = user.Plan,
["country"] = user.Country
}
},
explain: true,
cancellationToken);
bool enabled = await flags.IsEnabledAsync(
"checkout-redesign",
new EvaluationContext { UserId = user.Id },
cancellationToken);
CheckoutSettings? settings = await flags.GetValueAsync<CheckoutSettings>(
"checkout-settings",
new EvaluationContext { UserId = user.Id },
cancellationToken);
For console applications without DI, the SDK owns the transport:
using var flags = new FeatureFlags(managementKey, "production");
bool enabled = await flags.IsEnabledAsync("checkout-redesign",
new EvaluationContext { UserId = "user-123" }, defaultValue: false);
using var management = new FeatureFlagManagement(managementKey);
var definitions = await management.ListAsync();
Reuse these services for the application lifetime and dispose them at shutdown. DI manages service lifetimes automatically.
The existing FeatureFlagClient, FeatureFlagManagementClient, and their interfaces remain available for compatibility.
BaseUri is an advanced override for private deployments or tests; ordinary applications never configure an endpoint.
Manage flags
Use the same Management Key format. CanRead keys can list and get; create, update, delete, and configuration operations require CanWrite.
builder.Services.AddMaxlonaFeatureFlagManagement(options =>
{
options.ManagementKey = builder.Configuration["Maxlona:ManagementKey"]!;
});
var created = await management.CreateAsync(new CreateFeatureFlagRequest
{
Name = "checkout-redesign",
Project = "Main App",
Type = "release",
Description = "New checkout flow",
Tags = ["checkout", "q3"],
Variants =
[
FlagVariant.Create("on", true, "Feature enabled"),
FlagVariant.Create("off", false, "Feature disabled")
],
DependsOn = []
}, cancellationToken);
// A configuration can only be created in an environment assigned to the flag.
var environmentScope = await management.GetEnvironmentsAsync(created.Name, cancellationToken);
if (!environmentScope.Environments.Contains("production", StringComparer.OrdinalIgnoreCase))
{
await management.UpdateEnvironmentsAsync(created.Name, new UpdateFlagEnvironmentsRequest
{
Environments = [.. environmentScope.Environments, "production"]
}, cancellationToken);
}
await management.UpsertConfigurationAsync(created.Name, new UpsertFlagConfigurationRequest
{
Stage = "production",
Enabled = true,
DefaultVariantKey = "off",
RolloutPercentage = 25,
Rules =
[
new TargetingRule
{
Id = "pro-users",
Priority = 10,
Conditions = [TargetingCondition.Create("plan", "==", "pro")],
Allocations = [new VariantAllocation { VariantKey = "on", Percentage = 100 }]
}
]
}, cancellationToken);
Replacing an environment set can expose dependency conflicts. Pass DependencyResolution = "align" to align
related flags, or "remove_dependencies" to remove relationships that can no longer be satisfied. Removing an
environment also removes its saved configuration and notifies connected local clients, which refresh to safe-off.
Resilience
Every call goes through a Polly pipeline: 3 retries by default with exponential backoff and jitter, plus a per-attempt timeout (10s by default). All of it applies whether the client came from DI or was constructed by hand.
builder.Services.AddMaxlonaFeatureFlags(options =>
{
options.ManagementKey = builder.Configuration["Maxlona:ManagementKey"]!;
options.Stage = "production";
options.Timeout = TimeSpan.FromSeconds(10);
options.Retry.MaxRetryAttempts = 3; // 0 disables retries
options.Retry.BaseDelay = TimeSpan.FromMilliseconds(200);
options.Retry.MaxDelay = TimeSpan.FromSeconds(5);
options.Retry.HonorRetryAfter = true; // a 429 Retry-After header wins, capped by MaxDelay
});
What gets retried depends on whether repeating the request is safe:
| Failure | Reads, updates, deletes, upserts, evaluations | CreateAsync |
|---|---|---|
| Connection refused / DNS / host unreachable | retried | retried (nothing was delivered) |
| 408, 429, 503 | retried | retried (the server declined it) |
| 500, 502, 504 | retried | not retried (the write may have landed) |
| Connection reset mid-flight, timeout | retried | not retried |
| 400, 401, 403, 404, 409 | never retried | never retried |
Creating the same flag twice is not the same as creating it once, so a create is repeated only when the SDK can prove the request never reached the server.
Degrading gracefully
A feature flag is a configuration lookup, and an outage in a configuration lookup should not take down the feature it configures. Pass a fallback and an unreachable Maxlona stops being an exception:
// Returns false if Maxlona is unreachable or refuses the call. The failure is logged.
bool enabled = await flags.IsEnabledAsync("checkout-redesign", context, defaultValue: false, ct);
CheckoutSettings? settings = await flags.GetValueAsync("checkout-settings", context, Defaults.Checkout, ct);
// Or inspect the failure yourself.
var attempt = await flags.TryEvaluateAsync("checkout-redesign", context, explain: true, ct);
if (!attempt.Succeeded)
logger.LogWarning(attempt.Error, "Falling back to the default variant.");
The overloads without a fallback still throw, for callers that want to know.
Errors and diagnostics
| Exception | Meaning |
|---|---|
FeatureFlagApiException |
The API answered and refused. Carries StatusCode, ErrorCode, TraceId, ResponseBody, RetryAfter. |
FeatureFlagConnectionException |
The API could not be reached. Carries Problem, Guidance, Endpoint, Attempts. |
JsonException |
The response body was not the shape the SDK expected. |
Corporate networks break SDKs in ways that all look identical from .NET: every one of them arrives as the same opaque
HttpRequestException. The SDK classifies them instead and says what to check.
Problem |
Detected from | What the message tells you |
|---|---|---|
DnsFailure |
SocketError.HostNotFound |
The host did not resolve; run nslookup and check for a DNS filter. |
BlockedByFirewall |
connection refused, reset, unreachable, or silently dropped | Allow egress to the host and port in the host firewall, network ACL, and any NSG or security group; set HTTPS_PROXY if a proxy is in use. |
TlsInterception |
AuthenticationException during the handshake |
A TLS-inspecting proxy is re-signing traffic; trust its root certificate or bypass the host. |
ProxyAuthenticationRequired |
HTTP 407 | A proxy wants credentials. Maxlona itself never returns 407. |
InterceptedResponse |
a non-JSON body on a 2xx | A captive portal or web filter answered instead of Maxlona. |
Timeout |
the attempt timeout elapsed | Nothing answered, the signature of a firewall that drops rather than refuses. |
Check the whole path at startup, without throwing:
var report = await management.CheckConnectivityAsync(ct);
if (!report.IsHealthy)
logger.LogError("Maxlona unreachable: {Summary} {Guidance}", report.Summary, report.Guidance);
IsHealthy false with a null Problem means the network is fine and the API refused: a revoked key, a scope
mismatch, or an inactive subscription. Problem set means the traffic never got there.
Logging
The SDK writes its own Serilog file log, on by default, recording every retry, every classified failure, and its
guidance. It never touches Serilog.Log.Logger, so it cannot disturb the host application logging setup.
Logs default to AppContext.BaseDirectory/maxlona-FFlags, beside the running application.
Set options.Logging.Directory to an absolute directory or a relative path such as logs/flags.
Relative paths resolve from AppContext.BaseDirectory, regardless of the shell's working directory.
If the chosen directory is unwritable, file logging is skipped without failing flag operations.
Set options.Logging.Enabled = false to disable file logging, or supply your own Serilog logger.
Files roll daily as maxlona-featureflags-<date>.log, keeping 7 of them, capped at 16 MB each.
options.Logging.Enabled = true; // default
options.Logging.Directory = @"D:\logs\maxlona"; // override the location
options.Logging.MinimumLevel = LogEventLevel.Warning;
options.Logging.RetainedFileCountLimit = 7;
options.Logging.FileSizeLimitBytes = 16 * 1024 * 1024;
options.Logging.Logger = Log.Logger; // or fold into your own Serilog logger
Logging is best-effort by design: if the folder cannot be created or the file cannot be opened, the SDK degrades to writing nothing rather than failing the call. Losing a log line must never cost a flag evaluation.
Credential permissions
Every SDK request uses x-management-key. Grant only the required permissions:
CanEvaluate: resolve flag values throughPOST /flags/{name}; application scope is required and environment scope is optional.CanRead: list and retrieve flag definitions through the Management API.CanWrite: create, edit, configure, archive, and delete flags; read access is included.
The key is mandatory. The SDK validates it when the client is constructed, including at DI registration, so a
missing, blank, or unsendable key fails at startup rather than as a puzzling 401 on the first user request. A key
carrying whitespace or a non-ASCII look-alike character (the usual result of copying one out of a rich-text document)
is rejected explicitly, because HttpClient would otherwise drop the header and send the request with no credential
at all.
Keep the key in a secret store or environment variable. Do not commit it.
Coverage
The SDK supports the following flag evaluation and management operations:
| Endpoint | SDK method | Permission |
|---|---|---|
POST /flags/{key} |
EvaluateAsync, TryEvaluateAsync, IsEnabledAsync, GetValueAsync |
CanEvaluate |
GET /api/flags |
ListAsync |
CanRead |
GET /api/flags/{name} |
GetAsync, TryGetAsync, ExistsAsync |
CanRead |
POST /api/flags |
CreateAsync |
CanWrite |
PUT /api/flags/{name} |
UpdateAsync, ArchiveAsync, RestoreAsync |
CanWrite |
DELETE /api/flags/{name} |
DeleteAsync |
CanWrite |
POST /api/flags/{name}/configs |
UpsertConfigurationAsync |
CanWrite |
Use the Maxlona console to manage organisations, billing, users, groups, segments, webhooks, experiments, and approvals. These operations are not available through this SDK.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net8.0
- Microsoft.AspNetCore.SignalR.Client (>= 8.0.10)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Http (>= 8.0.1)
- Polly.Core (>= 8.5.2)
- Serilog (>= 4.2.0)
- Serilog.Sinks.File (>= 6.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
1.4.2 makes local evaluation reasons match the server's wording, naming the rollout percentage, stage, matched rule, and returned variant. A closed SignalR connection is now disposed before reconnecting.