# .NET quickstart

Source: https://www.sheepit.ai/docs/sdks/dotnet

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](https://www.nuget.org/packages/Sheepit.Sdk)
- Runs on: net8.0 · net10.0

## 1. Get an API key

Don't have an account yet? [Create one, free](https://www.sheepit.ai/signup). 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.

## 2. Install

```bash
dotnet add package Sheepit.Sdk --version 0.1.2
```

## 3. Fetch the ruleset

`SheepitFlags.cs`:

```csharp
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)]))];
}
```

## 4. 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;
    }
}
```

## 5. 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.
