← All platforms
Quickstart
.NET
Sheepit.Sdk evaluates feature flags in your .NET process, from a ruleset you fetch with a secret key. It is a preview package: flag evaluation only.
- Package
- Sheepit.Sdk
- Runs on
- net8.0 · net10.0
Get an API key
Don't have an account yet? Create one, free. Then open Settings → API Keys in the dashboard and copy a secret key (lp_sec_…). It has full access to your project, so keep it on your server.
Install
bash
dotnet add package Sheepit.Sdk --version 0.1.2Fetch the ruleset
SheepitFlags.cs
using System.Net.Http.Headers;
using System.Text.Json;
using Sheepit.Sdk;
public sealed class SheepitFlags
{
private readonly Dictionary<string, RulesetFlag> _flags;
private SheepitFlags(Dictionary<string, RulesetFlag> flags) => _flags = flags;
// GET v1/flags/ruleset with a SECRET key (lp_sec_…). Server-side only.
// http.BaseAddress must END IN "/": the path below is relative, so a base such as
// https://sheepit.example.com/api/ keeps its /api/ prefix.
public static async Task<SheepitFlags> FetchAsync(HttpClient http, string secretKey)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "v1/flags/ruleset");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
using HttpResponseMessage response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
using JsonDocument doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
JsonElement data = doc.RootElement.GetProperty("data");
// Refuse a snapshot this package was not built to bucket.
int version = data.GetProperty("bucketing_version").GetInt32();
if (version != Bucketing.Version)
throw new NotSupportedException($"ruleset bucketing_version {version}, SDK implements {Bucketing.Version}");
return new SheepitFlags(data.GetProperty("flags").EnumerateArray()
.Select(ToFlag)
.ToDictionary(f => f.Key));
}
public object? Evaluate(
string flagKey,
string? userId,
string? deviceId,
IReadOnlyDictionary<string, object?> context,
object? callerDefault)
{
// No identity -> the caller's default. Never skip the rollout instead.
string? identity = Bucketing.Identity(userId, deviceId);
if (identity is null) return callerDefault;
_flags.TryGetValue(flagKey, out RulesetFlag? flag);
return RulesetEvaluator.Evaluate(flag, identity, context, callerDefault).Value;
}
private static RulesetFlag ToFlag(JsonElement f) => new(
f.GetProperty("key").GetString()!,
f.GetProperty("value_type").GetString()!,
JsValue.FromJson(f.GetProperty("default_value")),
f.GetProperty("killed").GetBoolean(),
[.. f.GetProperty("rules").EnumerateArray().Select(r => new RulesetRule(
r.GetProperty("id").GetString()!,
r.GetProperty("sort_order").GetDouble(),
Conditions(r),
r.TryGetProperty("value", out JsonElement v) ? JsValue.FromJson(v) : null,
r.TryGetProperty("enabled", out JsonElement en) && en.ValueKind != JsonValueKind.Null
? en.GetBoolean()
: null))],
[.. f.GetProperty("rollouts").EnumerateArray().Select(r => new RulesetRollout(
r.GetProperty("id").GetString()!,
r.GetProperty("current_pct").GetDouble(),
Conditions(r),
r.TryGetProperty("value", out JsonElement v) ? JsValue.FromJson(v) : null,
// Absent status = active (null). A JSON null status is NOT active: map it to "".
!r.TryGetProperty("status", out JsonElement s) ? null
: s.ValueKind == JsonValueKind.Null ? "" : s.GetString(),
r.TryGetProperty("has_user_overrides", out JsonElement h) && h.GetBoolean()))]);
private static List<RuleCondition> Conditions(JsonElement owner) =>
[.. owner.GetProperty("conditions").EnumerateArray().Select(c => new RuleCondition(
c.GetProperty("field").GetString()!,
c.GetProperty("op").GetString()!,
[.. c.GetProperty("values").EnumerateArray().Select(JsValue.FromJson)]))];
}Evaluate a flag
csharp
public static class Example
{
public static async Task<bool> IsNewCheckoutOnAsync(HttpClient http, string secretKey, string userId)
{
// http.BaseAddress = https://api.sheepit.ai/ (trailing slash). Fetch once and reuse; this package does not poll.
SheepitFlags flags = await SheepitFlags.FetchAsync(http, secretKey);
var context = new Dictionary<string, object?>
{
["country"] = "AR",
["attributes"] = new Dictionary<string, object?> { ["plan"] = "pro", ["seats"] = 12d },
};
return flags.Evaluate("new_checkout", userId, deviceId: null, context, callerDefault: false) is true;
}
}Check that it worked
In the dashboard, create a boolean flag named new_checkout with its default value set to true, then run the call site: it returns true. Then, in the environment your secret key belongs to, add a rule to that flag with no conditions that serves false, fetch the ruleset again, and it returns false.
Worth knowing
- Preview (0.x). The package evaluates flags from a ruleset you fetch. It has no HTTP client of its own, no background refresh and no event sending, and its API may change before 1.0.
- The ruleset endpoint needs a secret key (lp_sec_…). Keep it on the server; publishable and dev keys are refused.
- FetchAsync makes one request. Nothing refreshes it for you: fetch at startup, then again on your own schedule to pick up flag changes.
- HttpClient.BaseAddress must end in a slash: https://api.sheepit.ai/ on Sheepit's hosted service, or your own install's API URL (for example https://sheepit.example.com/api/) when self-hosted.
- Context values follow JSON: null, bool, string, double, lists and dictionaries. Numbers must be double (12d, not 12). In 0.1.0 any other number type, such as decimal, was turned into text using the process's current culture (under de-DE, 1.5m became "1,5"), so comparisons could silently disagree with the server. 0.1.1 fixed this: those types now format the same way under every culture.
- A System.Text.Json JsonElement in the context is safe as of 0.1.2 (the evaluator unwraps it and never throws), but real .NET values are still recommended: a JsonElement whose backing document has been disposed still loses that one comparison. Deserialize into typed values up front, or convert each one with JsValue.FromJson.
- Fields the server fills in for you, such as group membership, are not available locally. A condition on an ordinary field you did not supply evaluates to false, including neq and not_in. Group membership is the exception: with no groups supplied, neq and not_in evaluate to true, so a rule meant to exclude a group matches everyone. Pass every field your rules target.
- Experiments and per-user rollout overrides are not applied locally. A flag bound to a running experiment can return a different value here than on the server, so keep those flags off local evaluation.
- It does not send events or exposures. Send them with the HTTP ingest API (POST /v1/ingest) or the Node.js server SDK.