---
title: "OpenFeature .NET Provider"
description: "ConfigDirector Provider for the OpenFeature .NET SDK"
canonical_url: "https://docs.configdirector.com/sdks/openfeature/dotnet"
---
# OpenFeature .NET Provider

> ConfigDirector Provider for the OpenFeature .NET SDK

<div hidden="true" dataTitleIcon="openfeature-dotnet">



</div>

<external-badges package-url="https://www.nuget.org/packages/ConfigDirector.OpenFeature.ServerProvider" registry="nuget" repo="dotnet-sdks/tree/main/src/ConfigDirector.OpenFeature.ServerProvider">



</external-badges>

## Introduction

The OpenFeature .NET Provider is intended to be used in combination with the [OpenFeature .NET SDK](https://openfeature.dev/docs/reference/sdks/server/dotnet/). The provider wraps the ConfigDirector [.NET SDK](https://docs.configdirector.com/sdks/server/dotnet).

The provider targets `net8.0` and `netstandard2.0`, so it runs on .NET 8 and later as well as on .NET Framework 4.6.2 and later. It requires version `2.14.1` or later of the OpenFeature .NET SDK.

The provider is thread safe. Register one instance when your application starts, and shut OpenFeature down when it stops. Evaluations read config state the provider already holds in memory, so they make no network calls on the request path.

In an ASP.NET Core application, the `OpenFeature.Hosting` package does that wiring for you. Everything on this page applies either way, see [ASP.NET Core](#aspnet-core) further below.

## Installation

The provider can be installed from NuGet: [https://www.nuget.org/packages/ConfigDirector.OpenFeature.ServerProvider](https://www.nuget.org/packages/ConfigDirector.OpenFeature.ServerProvider)

The OpenFeature .NET SDK (`OpenFeature`) and the ConfigDirector .NET SDK (`ConfigDirector.ServerSdk`) are included as dependencies.

<code-group>

```bash [.NET CLI]
dotnet add package ConfigDirector.OpenFeature.ServerProvider --version 1.8.0
```

```xml [.csproj]
<ItemGroup>
  <PackageReference Include="ConfigDirector.OpenFeature.ServerProvider" Version="1.8.0" />
</ItemGroup>
```

</code-group>

## Configure and initialize the client

1. Create an instance of the provider using your server SDK key. You can retrieve a server SDK key for each environment under `SDK keys` in the dashboard's navigation panel.
2. Set the OpenFeature provider.
3. Get a client instance from OpenFeature.

```csharp [ConfigDirectorSetup.cs]
using ConfigDirector.OpenFeature;
using OpenFeature;

// IMPORTANT: Do not commit the server SDK key to your source code, it is a secret value.
// Waits until the provider is initialized or it times out.
// If initialization times out, the provider will continue attempting to
// initialize in the background.
await Api.Instance.SetProviderAsync(new ConfigDirectorProvider("YOUR-SERVER-SDK-KEY"));
var client = Api.Instance.GetClient();
```

<warning>

Server SDK keys are secret values. Do not commit them to your source code repository. Provide them at runtime via environment variables instead.

</warning>

`SetProviderAsync` completes when the initial config state arrives or the configured timeout elapses, and it does not throw on a connection failure. Until config state arrives, evaluations return the default value with the `PROVIDER_NOT_READY` error code, and the provider continues to connect in the background. Check `client.ProviderStatus` to find out whether config state actually arrived:

```csharp [ConfigDirectorSetup.cs]
using OpenFeature.Constant;

var ready = client.ProviderStatus == ProviderStatus.Ready;
```

### Additional configuration options

Additional configuration options can be passed into the provider in the optional second argument of the constructor, which takes the same `ConfigDirectorClientOptions` the .NET SDK client does.

For example, the `Metadata` can be provided like this:

```csharp [ConfigDirectorSetup.cs]
using ConfigDirector;
using ConfigDirector.OpenFeature;

var provider = new ConfigDirectorProvider("YOUR-SERVER-SDK-KEY", new ConfigDirectorClientOptions
{
    Metadata = new Metadata { AppName = "YOUR-APP-NAME", AppVersion = "1.0.2" },
});
```

The provider accepts the same `Metadata`, `LoggerFactory`, `Connection`, and `Telemetry` options as the .NET SDK client, [refer to the additional configuration options section of the .NET SDK for a full list](https://docs.configdirector.com/sdks/server/dotnet#additional-configuration-options).

That includes [running without a connection](https://docs.configdirector.com/sdks/server/dotnet#run-without-a-connection): built with `new ConfigDirectorProvider(options)` and no server SDK key, the provider serves the values you give it without connecting to ConfigDirector, and if those values cannot be loaded OpenFeature puts the provider in its error state and evaluations return their in-code default values.

### Shut down

Shutting OpenFeature down closes the provider, which closes its connections and reports any pending telemetry:

```csharp [Program.cs]
await Api.Instance.ShutdownAsync();
```

## Retrieve config values

To retrieve config values, use the OpenFeature client:

```csharp [Program.cs]
var booleanValue = await client.GetBooleanValueAsync("my-config-key", false);

var stringValue = await client.GetStringValueAsync("my-string-config-key", "Default");
```

Each OpenFeature getter maps to a ConfigDirector config type:

<table>
<thead>
  <tr>
    <th>
      OpenFeature getter
    </th>
    
    <th>
      ConfigDirector config value
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        GetBooleanValueAsync
      </code>
    </td>
    
    <td>
      Boolean
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GetStringValueAsync
      </code>
    </td>
    
    <td>
      String or enum
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GetIntegerValueAsync
      </code>
      
      , <code>
        GetDoubleValueAsync
      </code>
    </td>
    
    <td>
      Number
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GetObjectValueAsync
      </code>
    </td>
    
    <td>
      JSON object or JSON array
    </td>
  </tr>
</tbody>
</table>

`GetObjectValueAsync` returns the config's JSON as an OpenFeature `Value`: a `Structure` for a JSON object or a list for a JSON array, with nested values inside. Numbers inside it are doubles, as OpenFeature represents every number. The in-code default value decides the expected shape: provide a structure for a JSON object, or a list for a JSON array. A config holding JSON of another shape, such as `42` or `null`, gives the in-code default value with the `TYPE_MISMATCH` error code.

```csharp [Program.cs]
using OpenFeature.Model;

var settings = await client.GetObjectValueAsync("my-json-config-key", new Value(Structure.Empty));
var theme = settings.AsStructure?.GetValue("theme").AsString;
```

For additional information regarding the OpenFeature client [refer to the OpenFeature .NET SDK documentation](https://openfeature.dev/docs/reference/sdks/server/dotnet/).

### Evaluation details

The detailed getters of the OpenFeature client, such as `GetBooleanDetailsAsync`, report why an evaluation produced the value that it did:

<table>
<thead>
  <tr>
    <th>
      Outcome
    </th>
    
    <th>
      Reason
    </th>
    
    <th>
      Error code
    </th>
    
    <th>
      Variant
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      A value was found
    </td>
    
    <td>
      <code>
        TARGETING_MATCH
      </code>
    </td>
    
    <td>
      
    </td>
    
    <td>
      The variation name, or the value ID
    </td>
  </tr>
  
  <tr>
    <td>
      A percentage rollout was skipped for a context without a targeting key
    </td>
    
    <td>
      <code>
        DEFAULT
      </code>
    </td>
    
    <td>
      
    </td>
    
    <td>
      The variation name, or the value ID
    </td>
  </tr>
  
  <tr>
    <td>
      The config carries no value
    </td>
    
    <td>
      <code>
        DEFAULT
      </code>
    </td>
    
    <td>
      
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      The config key is unknown
    </td>
    
    <td>
      <code>
        ERROR
      </code>
    </td>
    
    <td>
      <code>
        FLAG_NOT_FOUND
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      No config state has arrived yet
    </td>
    
    <td>
      <code>
        ERROR
      </code>
    </td>
    
    <td>
      <code>
        PROVIDER_NOT_READY
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      The value does not match the requested type
    </td>
    
    <td>
      <code>
        ERROR
      </code>
    </td>
    
    <td>
      <code>
        TYPE_MISMATCH
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
  
  <tr>
    <td>
      The provider has been shut down
    </td>
    
    <td>
      <code>
        ERROR
      </code>
    </td>
    
    <td>
      <code>
        PROVIDER_FATAL
      </code>
    </td>
    
    <td>
      
    </td>
  </tr>
</tbody>
</table>

The `Variant` is the name of the variation served, such as `treatment`, when the config has variations and that variation has a name. Otherwise it is ConfigDirector's ID for the value served, a stable hash you can send to analytics in place of the value itself; values served in offline mode have none. Before version 1.8.0 of the provider, the `Variant` was always the value ID, so an analytics tool that receives `Variant` now shows an experiment's evaluations under the variation names. In the last five cases, the in-code default value is returned.

To report an experiment to an analytics tool, follow the page for [Amplitude](https://docs.configdirector.com/integrations/analytics/amplitude), [Mixpanel](https://docs.configdirector.com/integrations/analytics/mixpanel), [Google Analytics 4](https://docs.configdirector.com/integrations/analytics/ga4), [Segment](https://docs.configdirector.com/integrations/analytics/segment) or [PostHog](https://docs.configdirector.com/integrations/analytics/posthog), which names the event that tool reads.

## Context

The context can be provided as the third argument to value getter methods of the OpenFeature client. The OpenFeature .NET provider evaluates targeting rules locally without additional network calls for different contexts.

```csharp [Program.cs]
using OpenFeature.Model;

var context = EvaluationContext.Builder()
    .SetTargetingKey("12345") // In OpenFeature, the targeting key represents the context's identifier
    .Set("name", "Example User")
    // Any arbitrary traits which can be referenced in targeting rules
    .Set("traits", Structure.Builder().Set("region", "North America").Build())
    .Build();

var booleanValue = await client.GetBooleanValueAsync("my-config-key", false, context);
```

To evaluate flags for an entity other than a user, such as a tenant, set the context kind with a top-level `kind` attribute:

```csharp [Program.cs]
var tenant = EvaluationContext.Builder()
    .SetTargetingKey("acme")
    .Set("kind", "tenant")
    .Set("name", "Acme Inc.")
    .Set("traits", Structure.Builder().Set("plan", "enterprise").Build())
    .Build();

var booleanValue = await client.GetBooleanValueAsync("my-config-key", false, tenant);
```

Leaving `kind` out means `user`. A `kind` inside `traits` is an ordinary trait, not the context kind. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and flags still resolve successfully, as if no context had been given. Context kinds are available from version 1.5.0 of the provider.

The evaluation context maps onto the ConfigDirector context as follows:

<table>
<thead>
  <tr>
    <th>
      OpenFeature evaluation context
    </th>
    
    <th>
      ConfigDirector context
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      The targeting key, or otherwise an <code>
        id
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        Id
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The top-level <code>
        kind
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        Kind
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The <code>
        name
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        Name
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The <code>
        traits
      </code>
      
       structure attribute
    </td>
    
    <td>
      <code>
        Traits
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      The boolean <code>
        anonymous
      </code>
      
       attribute
    </td>
    
    <td>
      <code>
        Anonymous
      </code>
    </td>
  </tr>
</tbody>
</table>

An evaluation context with neither a targeting key nor an `id` attribute skips percentage rollouts: a rollout does not apply to it, and the targeting rules after the rollout are tried, as they are after a conditional rule the context does not match. When none of them applies, the flag resolves to the default targeting rule value with the reason `DEFAULT`; a later rule that applies resolves with `TARGETING_MATCH`. A rule that serves a single value still applies to it. To keep an anonymous visitor in a rollout, give the context a stable targeting key, such as a session ID. Version 1.8.0 of the provider ended the evaluation at the skipped rollout and resolved to the default targeting rule value even when a later rule matched, and versions before 1.8.0 served every such context the first share of the rollout.

For additional information regarding the OpenFeature client [refer to the OpenFeature .NET SDK documentation](https://openfeature.dev/docs/reference/sdks/server/dotnet/).

## Events

The provider emits `PROVIDER_CONFIGURATION_CHANGED` whenever configs are updated on the dashboard or via the admin API, carrying the keys of the configs in the update:

```csharp [Program.cs]
using OpenFeature;
using OpenFeature.Constant;

Api.Instance.AddHandler(
    ProviderEventTypes.ProviderConfigurationChanged,
    payload => Console.WriteLine($"Configs updated: {string.Join(", ", payload?.FlagsChanged ?? [])}"));
```

It emits `PROVIDER_READY` when the initial config state arrives after `SetProviderAsync` has already returned.

## Track events

The provider implements OpenFeature's tracking API. `client.Track` reports to ConfigDirector that something happened in your application, such as a completed checkout, and ConfigDirector shows it on the Live feed and counts it for each variation of a running experiment; see [Tracked events](https://docs.configdirector.com/monitoring/activity#tracked-events).

```csharp [Program.cs]
using OpenFeature;
using OpenFeature.Model;

var client = Api.Instance.GetClient();

client.Track(
    "checkout-completed",
    EvaluationContext.Builder().SetTargetingKey("user-123").Build(),
    TrackingEventDetails.Builder().SetValue(49.9).Set("plan", "pro").Set("seats", 3).Build());
```

The `Value` of the tracking event details is sent as the event's value, and their other attributes as the event's details, with a `DateTime` sent as ISO 8601 text and a number that is not finite sent as null. The context the event happened for is the merged evaluation context: its targeting key, or otherwise its `id` attribute, and its `kind`.

An event counts for an experiment only when its evaluation context has the targeting key the experiment's flag was evaluated with, so track with the context you evaluate with. Without one, the event is still reported and counts for no experiment. For a context marked `anonymous`, the event is reported without the targeting key, so it counts for no experiment either.

The provider sends tracked events as the [.NET SDK](https://docs.configdirector.com/sdks/server/dotnet#track-events) does, with the same limits. OpenFeature's client throws on a null, empty or whitespace event name before the provider is called. Before version 1.8.0 of the provider, a `Track` call did nothing.

## ASP.NET Core

The `OpenFeature.Hosting` package registers OpenFeature with the host. It initializes the provider as the host starts, before the server begins listening, so no request is served config defaults while the first config state is still in flight, and it shuts the provider down when the host stops, which closes the ConfigDirector client.

```bash
dotnet add package OpenFeature.Hosting
```

Register the provider through the OpenFeature builder, and take `IFeatureClient` from the container wherever a config is read:

```csharp [Program.cs]
using ConfigDirector;
using ConfigDirector.OpenFeature;
using OpenFeature;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenFeature(openFeature =>
    openFeature.AddProvider(services => new ConfigDirectorProvider(
        builder.Configuration["ConfigDirector:ServerSdkKey"]!,
        new ConfigDirectorClientOptions
        {
            LoggerFactory = services.GetRequiredService<ILoggerFactory>(),
        })));

var app = builder.Build();

app.MapGet("/checkout", async (IFeatureClient client) =>
    await client.GetBooleanValueAsync("new-checkout", false));

app.Run();
```

<note>

`AddOpenFeature` drives its own `Api` instance rather than the static `Api.Instance`, so the provider registered this way is not reachable through `Api.Instance`. Take `IFeatureClient`, and `Api` if you need it, from the container instead.

</note>

Event handlers are registered on the same builder. They attach once the provider has initialized, so they see the updates that follow startup:

```csharp [Program.cs]
using OpenFeature.Constant;

builder.Services.AddOpenFeature(openFeature =>
{
    openFeature.AddProvider(_ => new ConfigDirectorProvider(builder.Configuration["ConfigDirector:ServerSdkKey"]!));

    openFeature.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, services =>
    {
        var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("ConfigDirector");
        return payload => logger.LogInformation("Configs updated: {Keys}", payload?.FlagsChanged);
    });
});
```

<warning>

Never build a provider per request. Each one holds its own ConfigDirector client and connection, initialization does network I/O, and a fresh provider serves defaults until its first config state arrives.

</warning>

## Test your code

Code that reads flags through OpenFeature is tested by swapping the provider, so it never has to change for a test and nothing from ConfigDirector is involved. The OpenFeature .NET SDK ships an in-memory provider for that, `InMemoryProvider` in `OpenFeature.Providers.Memory`, which serves the values your test sets.

Each flag is a `Flag<T>` built from its named variants and the default variant it resolves to:

```csharp [CheckoutTests.cs]
using OpenFeature;
using OpenFeature.Providers.Memory;

public class CheckoutTests : IAsyncLifetime
{
    public Task InitializeAsync() => Task.CompletedTask;

    public Task DisposeAsync() => Api.Instance.ShutdownAsync();

    [Fact]
    public async Task ShowsTheNewCheckout()
    {
        var provider = new InMemoryProvider(new Dictionary<string, Flag>
        {
            ["new-checkout"] = new Flag<bool>(new Dictionary<string, bool> { ["on"] = true, ["off"] = false }, "on"),
            ["max-items"] = new Flag<int>(new Dictionary<string, int> { ["default"] = 20 }, "default"),
        });
        await Api.Instance.SetProviderAsync(provider);
        var client = Api.Instance.GetClient();

        Assert.True(await client.GetBooleanValueAsync("new-checkout", false));
        Assert.Equal(20, await client.GetIntegerValueAsync("max-items", 0));

        await provider.UpdateFlagsAsync(new Dictionary<string, Flag>
        {
            ["new-checkout"] = new Flag<bool>(new Dictionary<string, bool> { ["on"] = true, ["off"] = false }, "off"),
        });
        Assert.False(await client.GetBooleanValueAsync("new-checkout", true));
    }
}
```

`UpdateFlagsAsync` replaces every flag, so a flag left out of it resolves to its default value with `FLAG_NOT_FOUND` afterwards, and emits `PROVIDER_CONFIGURATION_CHANGED` with the keys it held before and the keys it was given. A flag can also carry a context evaluator, a function from the evaluation context to the name of the variant to serve, for a test of code that targets specific users. `Api.Instance` is one global, so shut it down after each test with `ShutdownAsync`, as above, which lets the next test register its own provider. An ASP.NET Core application registers its provider through `AddOpenFeature`, as the section above shows; keep that registration in one place the test host can replace, so a test host registers the in-memory provider there instead.


## Sitemap

See the full [sitemap](https://docs.configdirector.com/sitemap.md) for all pages.
