Skip to content

C# SDK

The C# SDK is the in-code half of smplkit: one client resolves flags, reads config, controls log levels, and emits audit events. You manage the underlying resources separately — with the MCP tools or the Console — and the two halves meet at a shared id.

Install

bash
dotnet add package Smplkit.Sdk

Requires .NET 8.0+. The version published to NuGet is always the latest.

Initialize the client

One SmplClient exposes all four products as properties: client.Flags, client.Config, client.Logging, and client.Audit. The SDK key (sk_api_...) is read from the SMPLKIT_API_KEY environment variable or the [default] profile in ~/.smplkit — you rarely pass it in code. Set Environment once on the client (it scopes all flag/config/logger resolution); it is never a per-call argument.

csharp
using Smplkit;

using var client = new SmplClient(new SmplClientOptions
{
    Environment = "production",
    Service = "my-service",
});

// Optional: pre-warm definitions + the WebSocket so the first .Get() is instant.
await client.WaitUntilReadyAsync();

using var disposes the client (closing the WebSocket and flushing buffers) at end of scope. Flag declaration and .Get() are synchronous; lifecycle, logging install, and audit flush are async (...Async).

Flags

Use flags when you want a value that can change without a deploy — kill-switches, gradual rollouts, per-plan behavior. Declare a typed handle with a safe default, attach context, then .Get():

csharp
var checkout = client.Flags.BooleanFlag("checkout-v2", defaultValue: false);

// Ambient context, set once per request from middleware. The returned
// IDisposable reverts the context when disposed.
using var _ = client.SetContext(new[]
{
    new Context("user", "alice@acme.com", new Dictionary<string, object?>
    {
        ["plan"] = "enterprise",
    }),
});

if (checkout.Get())
    RenderNewCheckout();

The default is served if the flag is missing or smplkit is unreachable — pick the conservative value. .Get() is pure local JSON Logic evaluation; there is no network call per evaluation. C# supports both ambient context via client.SetContext(...) (an IDisposable scope) and a per-call override: checkout.Get(context: new[] { ... }).

→ Full reference (every language, all context modes, change listeners): Flags Runtime

Config

Use config for structured settings you want to change at runtime. Bind returns a live POCO that smplkit mutates in place as values change; GetValueOr reads a single value with a default:

csharp
// Live object — fields update in place when the config changes server-side.
var billing = client.Config.Bind("showcase-billing", new Billing(), parent: common);
Console.WriteLine(billing.Plan.MaxSeats);

// Single value with a default (never throws). Plain GetValue throws on a miss.
var slowQueryMs = client.Config.GetValueOr("showcase-database", "slow_query_threshold_ms", 500);

Subscribe("id") returns a live dictionary view (client.Config.Subscribe("common")["app.name"]) when you'd rather not declare a POCO.

Config Runtime

Logging

Use logging to control log levels from the server without redeploying. Wire smplkit into Microsoft.Extensions.Logging once with AddSmplkit, then call InstallAsync — that single call hands every discovered logger category to smplkit and applies the managed levels across every registered provider (Console, Debug, file sinks, Serilog-via-MEL):

csharp
using Microsoft.Extensions.Logging;
using Smplkit;
using Smplkit.Logging.Adapters;

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddConsole();
    builder.AddSmplkit(client);
});

await client.Logging.InstallAsync();

// Use loggers normally from here — levels are server-driven.
var dbLogger = loggerFactory.CreateLogger("MyApp.Db");
dbLogger.LogInformation("Server-managed level controls whether this is emitted");

Inside an ASP.NET Core / Generic Host app the wiring is the same: services.AddLogging(b => b.AddSmplkit(client)). Serilog is supported via SerilogAdapter (wired explicitly because Serilog has no per-provider factory).

Logging Runtime

Audit

Use audit to record an immutable trail of who did what. client.Audit.Events.Record(...) is fire-and-forget — it returns immediately and the SDK buffers, flushes, and retries in the background. Call FlushAsync for durability before a process exits (or in CLIs/tests):

csharp
using Smplkit;
using Smplkit.Audit;

client.Audit.Events.Record(new CreateEventInput
{
    EventType = "invoice.created",
    ResourceType = "invoice",
    ResourceId = "o-9876",
    Category = "billing",
    Severity = "WARN",
    Data = new Dictionary<string, object?>
    {
        ["snapshot"] = new Dictionary<string, object?> { ["total_cents"] = 4900 },
    },
});

// Force a flush so the event is durable before exit.
await client.Audit.Events.FlushAsync(TimeSpan.FromSeconds(2));

EventType, ResourceType, and ResourceId are required. ResourceType must not start with smpl. — that prefix is reserved and the server rejects it with a 403.

Emit audit events

The author + operate loop

Each product follows the same loop: write the SDK call, provision the resource with the MCP tools (create_flag / create_config / set_log_level), then verify (get_flag / get_config / query_events). The id in code is the key you provision — they must match.

For a checkout kill-switch, the code declares a boolean handle and evaluates it:

csharp
var checkout = client.Flags.BooleanFlag("checkout-v2", defaultValue: false);
if (checkout.Get())
    RenderNewCheckout();

Then provision and verify the flag — note the key equals the id used in code:

  • Provision: create_flag(key="checkout-v2", type="boolean", default=false)
  • Verify: get_flag(key="checkout-v2")

Until the flag exists, every .Get() returns the code-level default (false), so shipping the code first is safe.