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
                    
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="Maxlona.FeatureFlags.Offline" Version="1.4.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Maxlona.FeatureFlags.Offline" Version="1.4.2" />
                    
Directory.Packages.props
<PackageReference Include="Maxlona.FeatureFlags.Offline" />
                    
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 Maxlona.FeatureFlags.Offline --version 1.4.2
                    
#r "nuget: Maxlona.FeatureFlags.Offline, 1.4.2"
                    
#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 Maxlona.FeatureFlags.Offline@1.4.2
                    
#: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=Maxlona.FeatureFlags.Offline&version=1.4.2
                    
Install as a Cake Addin
#tool nuget:?package=Maxlona.FeatureFlags.Offline&version=1.4.2
                    
Install as a Cake Tool

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:

  1. 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.
  2. It requests the current manifest with the cached revision as an If-None-Match value. Unchanged configuration returns 304 Not Modified, so no configuration sections are downloaded.
  3. 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.
  4. It connects to /hubs/flags using the Streaming Key and listens for flagChanged. SignalR is an invalidation signal; evaluation data is always fetched from the authenticated configuration endpoints, never trusted from a notification payload.
  5. 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:

  • IsEnabledAsync returns false.
  • EvaluateAsync(..., explain: true) returns variant false, key off, and reason configuration_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 through POST /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 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. 
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
1.4.2 84 9/14/2026
1.1.0 86 9/14/2026
1.0.0 90 9/12/2026

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.