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
dotnet add package Smplkit.SdkRequires .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.
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():
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:
// 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.
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):
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).
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):
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.
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:
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.

