---
title: Configuration settings
summary: Configuration keys consumed by this library, retrieval methods, and runtime dependencies.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.EnvironmentClient
reference_type: package
---

# Configuration settings

`SAIF.NetCore.EnvironmentClient` is the in-repository implementation of `IEnvironmentClient`: it does not consume Environment API keys of its own, it *defines* how every other library's `GetSettingValue`/`GetSecureSetting`/`HasSetting` call is resolved. This page documents both the bootstrap configuration this library reads to configure itself and the key-addressing contract it exposes to every consumer.

## Configuration retrieval methods

| Method | Class / Interface | Namespace | Usage pattern |
| --- | --- | --- | --- |
| `Environment.GetEnvironmentVariable("EnvironmentApiUrl")` | `System.Environment` | `System` | Field initializer of `BaseEnvironmentClient.ServiceUrl`, read once per client instance before the constructor body and independent of `IConfiguration`. |
| `IConfiguration.GetSection(top).GetSection(key).Value` | `IConfiguration` | `Microsoft.Extensions.Configuration` | Imperative string read of `AppSettings:*` keys; missing → `""`. No binding to an options class. |
| `IConfiguration.GetValue<T>(key, default)` | `IConfiguration` (`ConfigurationBinder`) | `Microsoft.Extensions.Configuration` | Typed read with a code default, wrapped in the internal `ClientConfigBase.GetValue<T>` helper, which composes `EnvironmentClient:{SubSection}:{key}`. |
| `IConfiguration[key]` (indexer) | `IConfiguration` | `Microsoft.Extensions.Configuration` | Root-level string compare `config["COREAPIENABLED"] != "1"`. |
| `IHostEnvironment.ApplicationName` / `ContentRootPath` | `IHostEnvironment` | `Microsoft.Extensions.Hosting` | Host-supplied identity: application name used in every Environment API URL, and content root used for host inference. |
| Active Directory lookup | `System.DirectoryServices.AccountManagement` (`PrincipalContext`, `ComputerPrincipal`, `GroupPrincipal`) | `System.DirectoryServices.AccountManagement` | Determines whether the machine is a member of `Developer_Machine_Users`; gates every developer-only configuration path. |
| `File.Exists` / `File.ReadAllText` + `JsonConvert.DeserializeObject<ConfigSetting[]>` | `System.IO.File`, Newtonsoft | `System.IO`, `Newtonsoft.Json` | Developer override file (path from `AppSettings:EnvApiDeveloperSettingsOverride`); re-read on every non-secure value lookup. |

`IOptions<T>`/`Configure<T>`/`Bind`, `ConfigurationManager.AppSettings`, and `ApplicationSettingsBase` are not used by this library*.

## IConfiguration catalog

| Key (`Section:Key`) | Required | Default when absent | Type / accepted values | Purpose | Consuming class / member | Source |
| --- | --- | --- | --- | --- | --- | --- |
| `AppSettings:ApplicationHost` | Bootstrap — required on developer machines; ignored on non-developer machines | none (`""`) | string; must equal a host listed in some Environment API track (stored lowercased) | Forces the environment host (and therefore the track) on a developer machine; its presence also **defines** `IsDeveloperMachine()` = true. | `HostResolver.TryResolveHostFromAppConfig` | `HostResolver.cs` |
| `AppSettings:EnvApiDeveloperOverrideAllowed` | Bootstrap — optional | `true` (missing or empty); an unparsable value → `false` | `true`/`false` (case-insensitive) | When true, performs an AD lookup for `Developer_Machine_Users` on every startup; when false, the host and file overrides are ignored entirely and no AD call is made. | `HostResolver.IsConfigOverrideAllowed` | `HostResolver.cs` |
| `AppSettings:EnvApiDeveloperSettingsOverride` | Bootstrap — optional (developer only) | none → no override file | file path (absolute or relative; relative resolves against the process current directory*) | Path to the developer override JSON. Honored only if the file exists **and** `IsDeveloperMachine()`. | `BaseEnvironmentClient.ResolveDeveloperSettingsOverride` | `BaseEnvironmentClient.cs` |
| `EnvironmentClient:GetHostsByTrack:RetryCount` | Optional | `3` | int; `≤ 0` → no attempt, `EnvironmentClientException` thrown immediately with an empty log* | Number of attempts for the startup track fetch. | `GetHostsByTrackConfig.RetryCount` | `ClientConfigs/GetHostsByTrackConfig.cs` |
| `EnvironmentClient:GetHostsByTrack:RetrySleepMs` | Optional | `3000` | int (milliseconds); linear back-off, sleep `attempt × RetrySleepMs` after each failure | Back-off unit for the retry above; worst case with defaults is 3 s + 6 s + 9 s = 18 s plus HTTP timeouts*. | `GetHostsByTrackConfig.RetrySleepMs` | `ClientConfigs/GetHostsByTrackConfig.cs` |
| `COREAPIENABLED` (configuration root) | Required for `SecureEnvironmentClient` on non-developer machines | none → secure client construction fails unless `IsDeveloperMachine()` | exactly `"1"` (string compare; `"true"`, `"yes"`, `" 1"` all fail) | Server opt-in for secure (crypto) settings. | `SecureEnvironmentClient..ctor` | `SecureEnvironmentClient.cs` |

Keys that appear in sample `appsettings.json` test fixtures but that this library does **not** read: `AppSettings:ApplicationName` (a no-op, despite inviting the false belief that it sets the Environment API application identity)*.

## Environment variables catalog

| Variable | Read mechanism | Scope | Accepted values | Default when unset | Effect |
| --- | --- | --- | --- | --- | --- |
| `EnvironmentApiUrl` | `Environment.GetEnvironmentVariable("EnvironmentApiUrl")` — field initializer, evaluated before the constructor body and before any `IConfiguration` read | Process; **cannot** be supplied by `appsettings.json`, command-line args, user secrets, or prefixed env vars | Absolute base URL of the Environment API | none — `MissingEnvironmentUrlVariableException` at `Setup()`, and the singleton `IEnvironmentClient` cannot be constructed | Environment API base URL for every request this client makes |
| `COREAPIENABLED` (typically set as `ASPNETCORE_COREAPIENABLED`) | `IConfiguration["COREAPIENABLED"]` (indirect) | Depends on host configuration providers: ASP.NET Core web hosts chain the `ASPNETCORE_`-prefixed host configuration into app configuration, so both prefixed and unprefixed names work; console/Windows-service hosts built with `BuildSaifConsoleApp` only map the unprefixed name* | `"1"` only | unset → `SecureEnvironmentClient` throws unless `IsDeveloperMachine()` | Server-level opt-in that allows the secure (crypto) client to be constructed |

Code comment discrepancy: `SecureEnvironmentClient.cs` describes the alternative gate condition as debugging with a `VisualStudioDir` environment variable; the live code does not read `VisualStudioDir` — the alternative is `IsDeveloperMachine()` (AD group + `AppSettings:ApplicationHost`). The comment is stale.

## Bootstrap sequence (pre-DI → first lookup)

All of the following runs inside the `BaseEnvironmentClient` constructor, and because ServiceExtensions registers the client as a singleton, it runs once on first resolution of `IEnvironmentClient` and blocks that resolution.

| Step | Input consumed | Failure mode |
| --- | --- | --- |
| 1 | `EnvironmentApiUrl` (OS environment variable, read before the constructor body) | — |
| 2 | `IHostEnvironment.ApplicationName` | — |
| 3 | Validate `EnvironmentApiUrl` non-empty | `MissingEnvironmentUrlVariableException` |
| 4 | `HttpClient.SetClientInfo` with the machine name as a placeholder host | — |
| 5 | `EnvironmentClient:GetHostsByTrack:RetryCount`/`RetrySleepMs` | After `RetryCount` failures: `EnvironmentClientException` with the accumulated log |
| 6 | `GET /v2/tracks/{ApplicationName}/value` | Empty or failed → retried (step 5) |
| 7 | `AppSettings:EnvApiDeveloperOverrideAllowed` → AD `Developer_Machine_Users` → `AppSettings:ApplicationHost` → `ContentRootPath` → machine name | AD/domain unavailability is unhandled* |
| 8 | Resolve the track whose host list contains the resolved host | `EnvironmentClientException("No environment track found for host! Host value: {ApplicationHost}")` |
| 9 | `AppSettings:EnvApiDeveloperSettingsOverride` (file must exist **and** `IsDeveloperMachine()`) | Silently ignored when not applicable |
| 10 | `HttpClient.SetClientInfo` with `ApplicationHost`/`ApplicationName`; `TimeToWaitSeconds = 60` | — |
| 11 (`SecureEnvironmentClient` only) | `COREAPIENABLED == "1"` or `IsDeveloperMachine()` | `EnvironmentClientException("This server is not enabled for CORE APIs.")`, evaluated **after** steps 1–10 have already called the Environment API |

## Environment API key-addressing contract

This library reads no Environment API keys of its own — it calls only structural endpoints (tracks, settings-by-section, `lastRefreshed`, secure setting). The table below documents the key-addressing rules it imposes on every consumer key.

| Aspect | Rule |
| --- | --- |
| Composed cache key | `"{section}.{key}"`; the section is lowercased on both store and lookup, but the stored key portion keeps the casing returned by the Environment API — see "Case" below. Consumers' own catalogs record this as `<section>:<key>`. |
| Section omitted, `GetSettingValue(key)` | Resolved in the **track section**: `"{TrackName}.{key}"`. |
| Section omitted, `HasSetting(key)` | Resolved as the **bare key** (no track prefix) — can disagree with the rule above for the same key. |
| Named section | Loaded lazily from `/v2/settings/{app}/{host}/{section}/value` on first access and cached; a section returning zero settings is retried on next access. |
| Case | `BuildDictionaryKey` lowercases the **entire** composed lookup key (section and requested setting key alike), but `StoreSettingInDictionary` only lowercases the section when storing — the setting-key portion is stored exactly as `ConfigKey.Key` was returned by the Environment API. Lookup is therefore case-insensitive for the section, but **not** guaranteed case-insensitive for the key: a request whose key casing differs from the API's returned casing can fail to match*. |
| Refresh | At most every 60 s, `GET /settings/lastRefreshed` (an empty cache bypasses this throttle and always refreshes); a newer server timestamp clears the cache and reloads the root section (literal empty-string section, not the track section) plus every previously loaded section. |
| Secure key existence prerequisite | A secure value is fetched only when a non-secure entry with the same section/key exists in the settings cache; otherwise "No crypto data found". Every secure key must also be published as a (placeholder) non-secure setting in the same section*. |
| Secure key scope | Secure settings are addressed by **track**, non-secure by **host**. |

Consumers of this contract in the repository: `SAIF.NetCore.OIDC`, `SAIF.NetCore.AuthService`, `SAIF.NetCore.Logging`, `SAIF.NetCore.DBConnectionClient`, `SAIF.NetCore.Clients.UC4`, `SAIF.NetCore.Services.Messaging`, and `SAIF.NetCore.ServiceExtensions` (including the secure read `dbconnectionclient:connectionservicecreds`). Their individual keys are cataloged in each library's own configuration page, not here.

## Developer overrides

| File / flag | When applied | What it overrides | What it does **not** override |
| --- | --- | --- | --- |
| `AppSettings:ApplicationHost` | `EnvApiDeveloperOverrideAllowed` not false **and** the computer account is a member of AD group `Developer_Machine_Users` | The environment host, therefore the track for every non-secure and secure lookup; also flips `IsDeveloperMachine()` to true, which unlocks the file override below and bypasses `COREAPIENABLED` for `SecureEnvironmentClient` | — |
| Developer settings override file (path from `AppSettings:EnvApiDeveloperSettingsOverride`, conventionally `developersettings.json`) | File exists and `IsDeveloperMachine()`; re-read and re-parsed on **every** non-secure value lookup (parse errors are swallowed) | Values returned by `GetSettingValue`/`GetSettingValueAsync` (string and `<T>`), matched case-insensitively on `"{section}.{key}"` (or bare `key` when section is empty); the file's `track` field is ignored | `HasSetting` (an override-only key still reports `false`), `GetSectionSettings`, and all secure settings (which gate on `HasSetting` and go to the API); the cached Environment API values are untouched |

File shape (`ConfigSetting[]`, Newtonsoft, property names case-insensitive):

```json
[
  { "ConfigKey": { "track": "<ignored>", "section": "<section>", "key": "<key>" },
    "ConfigValue": { "value": "<override value>" } }
]
```

Neither override is ever a production configuration source — both require AD group membership of the machine account. On a non-domain-joined or Linux host the AD call is expected to throw unless `EnvApiDeveloperOverrideAllowed` is `false`*.

## Secure settings

This library reads no secure setting of its own; it **implements** secure retrieval for all consumers.

| Aspect | Base client | Secure client |
| --- | --- | --- |
| Availability | Denied for the eight `systemHashValue`-string overloads, which reach `RetrieveSecureSetting[Async]` directly and unconditionally throw `EnvironmentClientException("Operation not allowed! This application is restricted from retrieving secure settings.")`; the eight `SystemType` overloads compute the hash via the (null-registered) `ISystemHashService` first and throw `NullReferenceException` before ever reaching that denial, so not all 16 overloads surface the same exception | Allowed after the `COREAPIENABLED == "1"` or developer-machine construction gate |
| Authorization input (caller-supplied hash) | `systemHashValue` string overloads | Passed as `?systemHashValue={UrlEncoded}` query parameter |
| Authorization input (`SystemType` overloads) | — | `_systemHashService.Get(systemType)`, computed by the injected `ISystemHashService` |
| Endpoint | — | `GET /v2/secureSetting/{app}/{track}[/{section}]/{key}/value?systemHashValue={UrlEncoded systemHashValue}` (section is not URL-encoded) |
| Existence prerequisite | — | `HasSetting(key, section)` must be true in the non-secure cache |
| Caching | — | `SecureSettingCache` keyed `"{section}.{key}"` (case-sensitive); re-fetched when `LastRefreshed` (non-secure cache) is later than the item's fetch time; never cleared otherwise |
| Type conversion | — | `Convert.ChangeType(result, typeof(T))` |

## Transitive configuration

| Source | Requirement |
| --- | --- |
| `SAIF.NetCore.ServiceContracts` | None — interfaces, enums, and POCOs only; no configuration reads |
| `IHttpJsonClient` implementation (supplied by the consumer, typically `SAIF.NetCore.HttpClients`) | Any configuration the HTTP client needs (timeouts, auth headers) is owned by that package, not visible from this library** |
| `ISystemHashService` implementation (supplied by the consumer, typically `SAIF.NetCore.SystemHash`) | Hash inputs are the `SystemType` and the registering assembly; no configuration keys observed from this side* |
| Hosting process | `EnvironmentApiUrl`, and for the secure client `COREAPIENABLED`, must be provisioned on every server/container running **any** library depending on `IEnvironmentClient` |

## Configuration dependency risks

| Risk | Severity | Note |
| --- | --- | --- |
| Hard startup dependency on an OS environment variable outside `IConfiguration` | High | `EnvironmentApiUrl` cannot be supplied by `appsettings.json`, Key Vault, user secrets, or prefixed env vars; missing it cascades to every dependent SAIF NCore service* |
| Blocking network I/O in a DI constructor | High | First resolution of `IEnvironmentClient` performs HTTP calls with linear back-off (`Thread.Sleep`) — up to ~18 s plus timeouts with defaults* |
| AD lookup on every startup by default | High | `EnvApiDeveloperOverrideAllowed` defaults to `true`, so every host, including non-domain-joined/Linux containers, runs an AD lookup that is Windows-only and unhandled on failure* |
| `COREAPIENABLED` variable-name mismatch by host type | Medium | Console/Windows-service hosts need the unprefixed name; web-host guidance names the `ASPNETCORE_`-prefixed form* |
| `COREAPIENABLED` gate evaluated after network bootstrap | Low | A misconfigured server still performs the full Environment API handshake before failing |
| Silent empty-string semantics on missing settings | Medium | String overloads return `""`; generic `<T>` overloads throw instead — consumers get different failure modes depending on the overload they chose |
| Base client registered with a null `ISystemHashService` | Low | `SystemType` secure overloads on `BaseEnvironmentClient` would throw `NullReferenceException` instead of the intended "Operation not allowed!"* |
| Developer override scope is narrower than it looks | Low | `HasSetting`, `GetSectionSettings`, and secure settings ignore the override file; only `GetSettingValue*` sees it |
| System hash travels in the query string | Medium (security) | May be captured by proxies or HTTP client logging* |
| Clock-skew-sensitive refresh | Low | Client local time vs. server `lastRefreshed` can miss or over-trigger cache reloads, which also governs secure-cache expiry* |

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Security and authorization](security-authorization.md)
- [API reference](api/index.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts. Statements marked with `**` could not be confirmed; treat
> as unknown until verified.

## Sources
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/BaseEnvironmentClient.cs`
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SecureEnvironmentClient.cs`
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/HostResolver.cs`
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SecureSettingCache.cs`
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/ClientConfigs/ClientConfigBase.cs`
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/ClientConfigs/GetHostsByTrackConfig.cs`
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/Exceptions/MissingEnvironmentUrlVariableException.cs`
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient.Tests/TestData/appsettings.json`
