SOASAP .NET SDK

Local-first feature flag evaluation for ASP.NET Core, worker services, and console applications.

Overview

The SOASAP .NET SDK synchronizes feature flag configuration from SOASAP Cloud in the background and evaluates flags from an in-memory snapshot. Flag reads are synchronous, O(1), and never perform network or disk I/O on the hot path.

The SDK provides:

  • Local evaluation — dictionary lookups against an immutable snapshot
  • Real-time synchronization — Server-Sent Events (SSE) delta updates
  • Persistent cache — disk-backed snapshot for cold starts
  • Offline operation — last known snapshot when the network or control plane is unavailable
  • Explicit defaults — every getter accepts a code-defined fallback
  • Non-blocking startup — optional preload via IHostedService

Runtime characteristics

  • O(1) local evaluation — reads from an in-memory snapshot; no network requests during evaluation
  • Real-time SSE synchronization — background worker applies validated payloads with atomic configuration updates
  • Persistent cache — disk-backed snapshot for cold starts and restarts
  • Offline operation — last known snapshot when the network or control plane is unavailable
  • Explicit defaults — missing, mistyped, or unsynced keys return your code-defined fallback
  • Non-blocking startupPreloadFlags() starts synchronization without blocking host startup
Production behavior. Getters never throw. The SDK treats feature flags as secondary infrastructure and does not block or crash the host on sync failures.

Requirements

Supported target frameworks:

  • .NET 6, .NET 8, .NET 9
  • .NET Standard 2.0 (legacy .NET Framework compatible)

Supported application types:

  • ASP.NET Core (minimal APIs, controllers, middleware)
  • Worker Services (IHostedService)
  • Console applications
  • Blazor Server and WebAssembly

Runs on Linux, Windows, and macOS. Requires outbound HTTPS to api.soasap.com for SSE synchronization.

Installation

.NET CLI:

dotnet add package Soasap.Sdk

Package Manager Console:

Install-Package Soasap.Sdk

Package reference:

<PackageReference Include="Soasap.Sdk" Version="1.0.3" />

Verify installation

dotnet list package

Confirm Soasap.Sdk appears in the project package list before continuing.

Before you begin

You will need:

  • A SOASAP account
  • A project
  • An environment
  • An environment API key

If you have not completed these steps, start with Quick Start.

Installation flow

After your project and environment exist in the dashboard, install the NuGet package and register ISOASAPClient once at startup. The API key binds the process to a single environment. Synchronization runs in the background; application code evaluates flags locally.

Dependency injection setup

Register the client as a singleton and enable preload for ASP.NET Core and worker hosts:

using Soasap.Sdk;

var builder = WebApplication.CreateBuilder(args);

var apiKey = builder.Configuration["SOASAP_API_KEY"]
    ?? throw new InvalidOperationException("SOASAP_API_KEY is required.");

builder.Services
    .AddSoasap(apiKey)
    .PreloadFlags();

var app = builder.Build();
app.Run();
  • API key — environment-scoped secret sent as X-API-Key on SSE requests; identifies which configuration to synchronize
  • Singleton clientAddSoasap registers one ISOASAPClient per process
  • PreloadPreloadFlags() registers SoasapPreloadHostedService, which starts the SSE worker at host startup without blocking StartAsync

Inject ISOASAPClient into controllers, minimal API delegates, middleware, or background services.

Configuration

Read the API key from configuration and pass it to AddSoasap. The SDK does not bind IConfiguration automatically.

appsettings.json or appsettings.Development.json:

{
  "SOASAP_API_KEY": "soasap..."
}

Or a nested section:

{
  "Soasap": {
    "ApiKey": "soasap..."
  }
}
var apiKey = builder.Configuration["Soasap:ApiKey"]
    ?? throw new InvalidOperationException("Soasap:ApiKey is required.");

builder.Services.AddSoasap(apiKey).PreloadFlags();

Environment variable:

SOASAP_API_KEY=soasap...

Optional fluent configuration on the builder returned by AddSoasap:

builder.Services
    .AddSoasap(apiKey, baseUrl: "https://api.soasap.com")
    .WithCacheDirectory("/var/lib/myapp/soasap-cache")
    .OnError(ctx => logger.LogWarning(ctx.Exception, "SOASAP {Source}", ctx.Source))
    .PreloadFlags();

Evaluating flags

All getters read from the current in-memory snapshot. Wrong JSON types return the default you provide.

Flag type Method
BooleanGetBool
StringGetString
NumberGetNumber
JSONGetJson<T>

Boolean

var enabled = client.GetBool("new-checkout", false);

GetFlag is an alias for GetBool.

String

var theme = client.GetString("checkout-theme", "default");

Number

SOASAP number flags map to double. Use for integers, limits, rates, and thresholds.

var maxItems = client.GetNumber("max-items", 10);
var discountRate = client.GetNumber("discount-rate", 0.0);

JSON

Best practice. Use strongly typed classes whenever possible instead of dictionaries or dynamic objects. Strong typing improves discoverability, validation, refactoring safety, and maintainability.
public sealed class CheckoutSettings
{
    public bool Enabled { get; set; }
    public int TimeoutSeconds { get; set; }
}
var settings = client.GetJson(
    "checkout-config",
    new CheckoutSettings { Enabled = false, TimeoutSeconds = 30 });

JSON property names are matched case-insensitively — camelCase payloads from the dashboard deserialize into PascalCase C# properties without [JsonPropertyName] attributes. Invalid JSON or non-object/array values return the default. Deserialization errors return the default; getters do not throw.

Explicit defaults

Always pass a default on every read:

client.GetBool("new-checkout", false);

The default is returned when:

  • The flag key does not exist in the synchronized snapshot
  • The flag was deleted or disabled in the dashboard for this environment
  • The process starts before the first SSE payload arrives and no disk cache exists
  • A restored disk cache is empty or invalid
  • The flag value type does not match the getter (for example, a string flag read with GetBool)
  • Synchronization is temporarily delayed after deploy or reconnect

Explicit defaults are your production safety net. They define safe behavior when configuration is missing, stale, or not yet synchronized — without try/catch or null checks on the hot path.

Rule of thumb. If a flag controls production behavior, the default should be the safest possible outcome — usually false for feature gates and conservative values for limits.

Runtime architecture

  • SSE delivers full or delta flag payloads to a background worker
  • Valid payloads replace the in-memory snapshot atomically — readers never observe partial updates
  • Evaluation reads a thread-safe snapshot with no locks on the request path
  • No HTTP requests and no blocking disk I/O during GetBool, GetNumber, GetString, or GetJson

Implementation details on the hot path:

  • Snapshot replacement uses Interlocked.Exchange for atomic, lock-free updates
  • Reads use Volatile.Read to obtain the current snapshot reference without blocking
  • Concurrent readers continue against the previous snapshot until the swap completes

Persistent cache

On construction, SOASAPClient synchronously loads the last persisted JSON snapshot from disk before any network activity. SSE updates enqueue debounced disk writes (coalesced every 2–3 seconds).

Default cache locations:

PlatformPath
Windows%LocalAppData%\soasap\cache\soasap_cache_{hash}.json
Linux / macOS~/.local/share/soasap/cache/soasap_cache_{hash}.json (via LocalApplicationData)

Override with .WithCacheDirectory("/custom/path"). Each API key maps to a separate cache file (hash derived from the key).

Deploy and restart — rolling deploys and pod restarts hydrate from disk immediately; SSE reconnects in the background. First deploy on a new machine without cache returns defaults until the first successful sync.

Offline operation

Reads are decoupled from sync. Flag evaluation continues normally during outages because reads never depend on network availability. Only synchronization is affected during disconnects.

The SDK continues evaluating flags from the last known snapshot when:

  • Internet connectivity is unavailable
  • SOASAP Cloud returns errors or is unreachable
  • The SSE connection drops (proxy timeout, firewall, transient network failure)

The background worker reconnects with exponential backoff. Reads are unaffected during disconnect — only future updates are delayed.

Stale configuration is preferable to unavailable configuration: the application keeps running with the last known values and your code defaults.

ScenarioBehavior
API unavailableServes stale cached snapshot
SSE disconnectedKeeps last known snapshot; reconnects with backoff
First startup, no cacheReturns explicit defaults until first sync
Invalid SSE payloadPayload ignored; snapshot unchanged
Disk cache failureIn-memory mode continues; error reported via OnError

Preload and startup behavior

PreloadFlags() (alias: UseBackgroundWorker()) is recommended for production ASP.NET Core and worker hosts.

With preload:

  1. Host starts; disk cache loads synchronously during client construction
  2. SoasapPreloadHostedService.StartAsync calls EnsureWorkerStarted() and returns immediately — no network await
  3. SSE worker connects and applies updates in the background
  4. The application accepts traffic while synchronization completes

Without preload (lazy mode):

builder.Services.AddSoasap(apiKey);

No background worker starts until the first flag read. The first evaluation uses the disk cache (if present) or your explicit defaults while the SSE worker starts in the background. Prefer preload for predictable synchronization timing after deploy.

Best practices

  • Register once — single AddSoasap call at the composition root.
  • Use dependency injection — inject ISOASAPClient; do not construct SOASAPClient manually.
  • Reuse the client — one singleton per process and environment.
  • Store API keys securely — see Recommended configuration.
  • Enable preload.PreloadFlags() for non-blocking background sync at startup.
  • Always pass explicit defaults — safe behavior when keys are missing or not yet synchronized.
  • Do not create clients per request — avoids duplicate SSE connections and memory overhead.
  • Use environment-specific API keys — Development, Staging, and Production hold independent configuration.

Common issues

Flag always returns the default

  • Verify the API key matches the intended environment
  • Confirm the flag key exists and is enabled in that environment
  • Check the flag type matches the getter (GetBool for boolean flags)

Dashboard changes do not appear

  • Verify outbound HTTPS to api.soasap.com and SSE is not blocked by a proxy or firewall
  • See SSE Disconnected

Wrong value returned

  • Verify the flag value in the selected environment — Development and Production are independent
  • Confirm the deployed process uses the API key for that environment

Multiple SDK instances

Symptoms

  • Multiple SSE connections from the same process or deployment
  • Increased memory usage
  • Duplicate synchronization traffic

Resolution — register SOASAP once as a singleton through dependency injection and reuse the same ISOASAPClient throughout the application. Do not call AddSoasap more than once or construct SOASAPClient manually in request handlers.

Startup synchronization delays

Expected with preload: the host starts immediately; the first SSE payload may arrive seconds after boot. Until then, evaluation uses the disk cache or explicit defaults. This is by design — startup is never blocked on sync.

Complete example

The following example demonstrates the recommended production setup for ASP.NET Core applications, including configuration, dependency injection, preload, and runtime flag evaluation.

using Soasap.Sdk;

var builder = WebApplication.CreateBuilder(args);

var apiKey = builder.Configuration["SOASAP_API_KEY"]
    ?? throw new InvalidOperationException(
        "Set SOASAP_API_KEY via environment variable or user secrets.");

builder.Services
    .AddSoasap(apiKey)
    .PreloadFlags();

var app = builder.Build();

app.MapGet("/checkout", (ISOASAPClient flags) =>
{
    if (!flags.GetBool("new-checkout", false))
    {
        return Results.Json(new { mode = "legacy" });
    }

    var maxItems = (int)flags.GetNumber("max-items", 10);
    var theme = flags.GetString("checkout-theme", "default");

    return Results.Json(new { mode = "new", maxItems, theme });
});

app.MapGet("/health/flags", (ISOASAPClient flags) =>
{
    var maintenance = flags.GetBool("maintenance-mode", false);
    return maintenance ? Results.StatusCode(503) : Results.Ok();
});

app.Run();

Production recommendations

  • Store API keys in a secret manager; inject via environment variables at runtime
  • Use separate API keys per environment — never share Production keys with Development builds
  • Never commit Production keys to source control or embed them in container images unnecessarily
  • Monitor outbound connectivity and SSE health; alert on prolonged disconnect in Production
  • Keep explicit defaults in code for every flag read — defaults define behavior during outages
  • Register .OnError in Production to log background sync, disk, and parser failures without affecting reads

Next steps

  • Feature Flags — flag types, defaults, and naming
  • Architecture — local evaluation, SSE, cache, and offline operation
  • API Keys — issuing, rotating, and securing environment keys
  • Other SDKs — Node.js, Python, React, and more

Deep dives: Preload Flags, Persistent Cache, Offline Behavior.