> ## Documentation Index
> Fetch the complete documentation index at: https://omniflagsdoc.omniretail.app/llms.txt
> Use this file to discover all available pages before exploring further.

# .NET SDK

> Feature flags for ASP.NET Core 8+ services with hosted service integration

## Installation

```bash theme={null}
dotnet add package OmniFlags.Sdk
```

**Requirements:** .NET 8+, ASP.NET Core (for the hosted service integration)

## Setup

Register OmniFlags in `Program.cs`. The SDK key is the only thing you configure locally. Polling intervals and CDN settings come down with the snapshot.

```csharp theme={null}
// Program.cs
builder.Services.AddOmniFlags(options =>
{
    options.SdkKey = builder.Configuration["OmniFlags:SdkKey"]
        ?? throw new InvalidOperationException("OmniFlags:SdkKey is required.");
});
```

Store the SDK key in `appsettings.json`. Use environment-specific overrides (`appsettings.Production.json`) or secret management to keep production keys out of source control.

```json theme={null}
{
  "OmniFlags": {
    "SdkKey": "sk_live_..."
  }
}
```

`AddOmniFlags` registers:

* **`OmniFlagsClient`** as a singleton in the DI container
* **A hosted service** that fetches the initial snapshot at startup, before the application begins accepting traffic

By the time the first request arrives, the flag snapshot is loaded and `OmniFlagsClient` is ready to evaluate.

***

## Evaluating flags

Inject `OmniFlagsClient` into services, controllers, or minimal API endpoints. The client is thread-safe and designed to be shared as a singleton.

### In a service class

```csharp theme={null}
public class CheckoutService
{
    private readonly OmniFlagsClient _flags;

    public CheckoutService(OmniFlagsClient flags)
    {
        _flags = flags;
    }

    public OrderResult PlaceOrder(CheckoutRequest req)
    {
        var ctx = new EvaluationContext { CustomerId = req.CustomerId };
        var chargeDeliveryFee = _flags.IsEnabled("checkout.charge-delivery-fee", ctx);

        return new OrderResult
        {
            DeliveryFee = chargeDeliveryFee ? CalculateFee(req) : 0m,
        };
    }
}
```

### In a minimal API endpoint

```csharp theme={null}
app.MapGet("/checkout", (string customerId, OmniFlagsClient flags) =>
{
    var ctx = new EvaluationContext
    {
        CustomerId = long.TryParse(customerId, out var id) ? id : null,
    };

    var chargeDeliveryFee = flags.IsEnabled("checkout.charge-delivery-fee", ctx);

    return Results.Ok(new { chargeDeliveryFee });
});
```

***

## Client API

### `IsEnabled` — boolean flags

```csharp theme={null}
bool IsEnabled(string flagKey, EvaluationContext? context = null, bool defaultValue = false)
```

Returns the flag value as a `bool`. The `defaultValue` is returned if the flag is not found, is disabled, or the client is not yet ready.

```csharp theme={null}
var chargeDeliveryFee = client.IsEnabled("checkout.charge-delivery-fee", ctx);
```

***

### `GetString` — string flags

```csharp theme={null}
string GetString(string flagKey, string defaultValue, EvaluationContext? context = null)
```

```csharp theme={null}
var bannerColour = client.GetString("product-listing.promo-banner-colour", "blue", ctx);
```

***

### `GetNumber` — number flags

```csharp theme={null}
double GetNumber(string flagKey, double defaultValue, EvaluationContext? context = null)
```

```csharp theme={null}
var maxCartItems = client.GetNumber("cart.max-items", 20, ctx);
```

***

### Detail variants

Every typed method has a `*Detail` variant that returns the full `EvaluationResult<T>`. Use it when you need to inspect why a value was returned; useful for logging, analytics, or debugging evaluation logic.

```csharp theme={null}
bool IsEnabledDetail(string flagKey, bool defaultValue, EvaluationContext? context = null)
    → EvaluationResult<bool>

string GetStringDetail(string flagKey, string defaultValue, EvaluationContext? context = null)
    → EvaluationResult<string>

double GetNumberDetail(string flagKey, double defaultValue, EvaluationContext? context = null)
    → EvaluationResult<double>
```

```csharp theme={null}
var result = client.IsEnabledDetail("checkout.charge-delivery-fee", false, ctx);

logger.LogInformation(
    "Flag {Key} → {Value} (reason: {Reason}, rule: {RuleId})",
    "checkout.charge-delivery-fee",
    result.Value,
    result.Reason,
    result.RuleId
);
```

| Field       | Type               | Description                                                                        |
| ----------- | ------------------ | ---------------------------------------------------------------------------------- |
| `Value`     | `T`                | The resolved flag value                                                            |
| `Variant`   | `string?`          | The matched variant key, or `null` for boolean rollouts                            |
| `Reason`    | `EvaluationReason` | Why this value was returned; see [reason codes](/concepts/evaluation#reason-codes) |
| `RuleId`    | `string?`          | The ID of the targeting rule that matched, if any                                  |
| `ErrorCode` | `ErrorCode?`       | Set when `Reason` is `Error`                                                       |

***

## Evaluation context

Construct an `EvaluationContext` with the user and session attributes available at the call site. All properties are optional. Include only what's relevant to your targeting rules.

```csharp theme={null}
var ctx = new EvaluationContext
{
    CustomerId = 12345,
    Country    = "Nigeria",
    City       = "Lagos",
    Platform   = "api",
    AppVersion = "3.1.0",
};

// Pass custom attributes for rule matching
ctx["plan"] = "enterprise";
ctx["tier"] = "gold";
```

| Property           | Type      | Notes                                                     |
| ------------------ | --------- | --------------------------------------------------------- |
| `CustomerId`       | `long?`   | Primary bucketing key for rollout and traffic splits      |
| `AgentId`          | `long?`   | Secondary bucketing key; used when `CustomerId` is absent |
| `BusinessId`       | `long?`   | Tertiary bucketing key                                    |
| `BusinessBranchId` | `long?`   |                                                           |
| `Country`          | `string?` | Full country name (e.g. `"Nigeria"`, `"Ghana"`)           |
| `City`             | `string?` |                                                           |
| `Platform`         | `string?` | `web`, `ios`, `android`, `api`, etc.                      |
| `AppVersion`       | `string?` | Semver string                                             |

Additional key-value pairs set on the context dictionary are available for custom attribute matching in targeting rules, including dot-path traversal (e.g., `ctx["user.plan"] = "enterprise"`).

<Warning>
  Rollout and traffic splits require a bucketing key (`CustomerId`, `AgentId`, etc.) to produce a deterministic result. If no bucketing key is present, the SDK returns the flag's default value with `ErrorCode.MissingTargetingKey`.
</Warning>

***

## Startup behaviour

`AddOmniFlags` registers a hosted service that fetches the initial snapshot during `IHostedService.StartAsync`, before the application begins serving traffic. This means:

* **Flags are always ready on the first request.** No warm-up period, no stale defaults on startup.
* **Startup failure does not block the application.** If the CDN is unreachable at startup, the hosted service logs the error and the client falls back to an empty snapshot. The application starts, and evaluation returns caller-supplied defaults until the next successful poll.

Background polling continues throughout the application's lifetime. The polling interval is set server-side and delivered in the snapshot.

## Shutdown

`OmniFlagsClient` implements `IAsyncDisposable`. The DI container disposes the singleton as part of the host shutdown sequence. No manual teardown is required.
