Skip to content

.NET client

Package: Nona.Client

OpenFeature provider package: Nona.OpenFeature.Provider

Targets:

  • netstandard2.0
  • net8.0

The .NET client is a good fit for:

  • ASP.NET applications
  • worker services
  • console tools
  • backend services that want a direct typed integration
Terminal window
dotnet add package Nona.Client

Before wiring the backend:

  1. open Projects
  2. open the backend service project
  3. select the target environment such as production
  4. create the parameter or flag you want to read
  5. create an API key in the API Keys section
  6. use server scope for backend-only reads whenever possible

For a first backend test, create Features:UseLegacySearch as a boolean entry or App:Settings as a JSON entry.

Terminal window
nona entries set \
--project storefront \
--environment production \
--key Features:UseLegacySearch \
--value false \
--scope server \
--content-type boolean
nona keys create \
--project storefront \
--name "API service" \
--scope server \
--environment production

The default client reads working parameters, so no release is required for this example. Publish a release only when you enable release reads; set it active if you omit ReleaseVersion.

using Nona.Client;
var client = new NonaClient(
"https://nona.example.com",
"production",
apiKey: Environment.GetEnvironmentVariable("NONA_API_KEY"));
var checkout = await client.GetStringValueAsync("Features:Checkout");
var checkoutEnabled = checkout == "true";

For feature flags, you will often prefer boolean entries and either metadata inspection or OpenFeature depending on how your application is structured.

var checkout = await client.GetConfigValueAsync("Features:UseLegacySearch");
var useLegacySearch =
checkout.ContentType == "boolean" &&
string.Equals(checkout.Value, "true", StringComparison.OrdinalIgnoreCase);

That keeps the application aligned with the logical type stored in Nona.

var value = await client.GetConfigValueAsync("Features:Checkout");
Console.WriteLine(value.Value);
Console.WriteLine(value.ContentType);

ContentType is one of text, number, boolean, or json.

This is useful when one service reads mixed config and needs to branch on the value type.

var value = await client.TryGetConfigValueAsync("Missing:Key");
if (value is null)
{
Console.WriteLine("Key was not found");
}

This is helpful for optional settings or for gradual rollout of new keys across environments.

By default, reads use working parameters. Set UseReleases when constructing the client to read immutable release snapshots instead.

Set ReleaseVersion to pin the client to an exact release or release line:

var client = new NonaClient(new NonaClientOptions
{
BaseAddress = new Uri("https://nona.example.com"),
EnvironmentId = "production",
ApiKey = Environment.GetEnvironmentVariable("NONA_API_KEY"),
UseReleases = true,
ReleaseVersion = "1.1.x"
});

Use an exact version such as 1.1.0 for a fixed snapshot. Use a line such as 1.1.x to read the highest patch in that line.

Omit ReleaseVersion while keeping UseReleases = true to follow the environment’s active release. If no active release exists, reads fail with 409 and ErrorCode == "active_release_not_configured"; they never fall back to working parameters. Source and selector are fixed for the client’s lifetime, so construct another client to use a different source or selector. When UseReleases is false (the default), ReleaseVersion is normalized and retained but ignored for requests and cache identity.

Fetch every readable value at startup, or restrict the snapshot to keys that begin with a prefix:

IReadOnlyDictionary<string, NonaConfigValue> all = await client.GetAllValuesAsync();
IReadOnlyDictionary<string, NonaConfigValue> features =
await client.GetAllValuesAsync("Features:");

Prefixes may contain ASCII letters, digits, colons, dots, underscores, and dashes, and matching is case-insensitive. Pass null or an empty string for an unfiltered snapshot. Any other character produces NonaClientException with StatusCode == HttpStatusCode.BadRequest; failed responses are not cached. Bulk reads cache an ETag independently for each release and normalized prefix, reuse the snapshot after 304 Not Modified, and prime single-key reads only for returned keys. The snapshots share the configured memory/LRU budget and returned dictionaries are defensive copies.

  • GetAllValuesAsync(string? prefix = null, CancellationToken cancellationToken = default)
  • GetConfigValueAsync(string key, CancellationToken cancellationToken = default)
  • TryGetConfigValueAsync(string key, CancellationToken cancellationToken = default)
  • GetStringValueAsync(string key, CancellationToken cancellationToken = default)
  • GetJsonValueAsync<T>(string key, JsonTypeInfo<T> jsonTypeInfo, CancellationToken cancellationToken = default)

GetJsonValueAsync<T> uses source-generated JsonTypeInfo<T>.

using System.Text.Json.Serialization;
using Nona.Client;
var settings = await client.GetJsonValueAsync(
"App:Settings",
AppJsonContext.Default.AppSettings);
public sealed record AppSettings(string Region, int MaxItems);
[JsonSerializable(typeof(AppSettings))]
internal partial class AppJsonContext : JsonSerializerContext
{
}

JSON works well when a backend service consumes a cluster of related settings together.

Example Nona setup:

  • key: App:Settings
  • content type: json
  • scope: server

The .NET client caches values in memory by default for 30 seconds. CacheTtl changes the cache lifetime and must be greater than zero; CacheMemoryLimitMegabytes defaults to 5 and must also be greater than zero. The current client does not expose a cache-disable option.

Set AllowStaleCache to return an expired cached value while the client refreshes it in the background.

var client = new NonaClient(new NonaClientOptions
{
BaseAddress = new Uri("https://nona.example.com"),
EnvironmentId = "production",
ApiKey = Environment.GetEnvironmentVariable("NONA_API_KEY"),
CacheTtl = TimeSpan.FromSeconds(30),
CacheMemoryLimitMegabytes = 5,
AllowStaleCache = true
});

Use AllowStaleCache carefully. It can improve resilience and smooth over transient failures, but it also means the application may temporarily serve an older value while refresh happens in the background.

If a .NET read fails:

  1. confirm the EnvironmentId matches the Nona environment name
  2. confirm that UseReleases selects the intended source
  3. in release mode, confirm the expected release is active or configure ReleaseVersion
  4. confirm the API key belongs to the same project as the entry
  5. confirm the key scope can read the entry scope
  6. try the same entry once with HTTP to separate transport issues from application code

A practical first integration looks like this:

  1. read one operational flag at startup or request time
  2. change it once in admin
  3. confirm the service sees the change
  4. tune CacheTtl only after the read path is working
  5. move to OpenFeature when the service becomes flag-oriented

Use the .NET client when you want:

  • a direct Nona integration in C#
  • built-in cache behavior
  • typed JSON reads
  • a simpler path than building your own HTTP wrapper

Use HTTP instead when you only need a minimal raw request path.

Install the optional provider package:

Terminal window
dotnet add package Nona.OpenFeature.Provider
using Nona.Client;
using Nona.OpenFeature.Provider;
using OpenFeature;
var nona = new NonaClient(
"https://nona.example.com",
"production",
apiKey: Environment.GetEnvironmentVariable("NONA_API_KEY"));
const string domain = "nona-production";
await Api.Instance.SetProviderAsync(
domain,
new NonaOpenFeatureProvider(nona));
var featureClient = Api.Instance.GetClient(domain);
var enabled = await featureClient.GetBooleanValueAsync("Features:Checkout", false);

If your team wants a more flag-oriented, vendor-neutral integration surface, continue with OpenFeature.