---
title: Load section
summary: OP-03 operation detail page.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.EnvironmentClient
reference_type: package
---

# Load section

| Field | Value |
| --- | --- |
| Owner | `LoadSection`/`LoadSectionAsync` → `LoadSettings[Async]` → `HandleGetConfigurationSettingsResults` → `StoreSettingInDictionary` |
| Trigger | Called internally by [Get setting value](get-setting-value.md), [Has setting](has-setting.md), and [Get section settings](get-section-settings.md) |
| Inputs | `section` (the caller's literal string; an empty string denotes a distinct "root" section key, it is **not** the same as the application's track section) |
| Outputs | New/updated entries in the single global `Settings` dictionary, keyed `"{section}.{key}"` (or just `key` when `section` is a literal empty string — the "root" case reached directly by [Has setting](has-setting.md)'s optional `section` parameter, which defaults to `""`; `GetSectionSettings(string section)` takes a **required** `section` argument with no default, so it never resolves to `""` implicitly, but a caller that explicitly passes `""` reaches this same root-loading path). Keys loaded via the resolved track name (the substitution [Get setting value](get-setting-value.md) step 1 performs for a blank/omitted section) are stored **prefixed** with the lowercased track name, not bare. Also updates the section's membership in the `Sections` `HashSet<string>` — but **only** when a fetch for that `section` succeeds and returns 1+ settings; the root/empty section is never added, so it is never short-circuited by step 3 and can be reloaded on every call that reaches it; a **zero-result** fetch for a named section is self-healing (removed from `Sections` if present, so it is retried on the next call); a **thrown exception** during a named section's reload leaves `Sections` unchanged, so a section already marked loaded before a global refresh (which clears `Settings` first) can remain marked loaded with no corresponding `Settings` entries until the next global refresh succeeds for it (see step 7) |

## Steps
1. Call `CheckForUpdates()` unconditionally — every `LoadSection` call checks whether a *global* cache refresh is due, regardless of whether this particular section is already loaded.
   - 1.1. An empty global `Settings` cache (including the very first call) always triggers a reload with no server probe, and in that case `LastCheckForRefresh` is **not** updated (it is only set inside the server-probe branch); otherwise a server probe (`GET /settings/lastRefreshed`) runs at most once every 60 seconds, updates `LastCheckForRefresh = DateTime.Now`, and triggers the reload only if the returned server timestamp is later than the client's current `LastRefreshed` — a due probe does not itself guarantee a reload.
   - 1.2. A triggered reload acquires `SectionsSemaphore`, sets `LastRefreshed = DateTime.Now` **before** any loading happens (not after a successful reload completes), clears the entire global `Settings` dictionary, then reloads the literal-empty-string root section plus every section previously recorded in `Sections` — this reload is not atomic, so a failure partway through can leave the cache empty or only partially repopulated. `Sections` itself is never cleared by this reload (only `Settings` is), so a named section that was already recorded in `Sections` before the refresh stays recorded even if its individual reload throws; see step 7. Only a **nonempty** section name reaching `HandleGetConfigurationSettingsResults` with 1+ returned settings is (re-)added to `Sections` and thereby marked loaded for this new cache generation; the root/empty section is never marked loaded, so it reloads again on the very next call that reaches it.
   - > **Origin**: Steps 1.1–1.2 are implemented in [Refresh check and cache reload](refresh-check-and-cache-reload.md) and executed on behalf of this operation.
2. Compute `key = GetSectionKey(section)` (`section.ToLower()`, with no null-guard — a `null` section throws `NullReferenceException` here rather than aliasing to root).
3. If `key` is already present in the `Sections` `HashSet<string>`, return immediately without a network call. The root/empty section (`key == ""`) is never added to `Sections` (see step 6), so this guard only ever short-circuits a **nonempty** section whose most recent fetch succeeded with 1+ settings: such a section is loaded from the Environment API at most once between cache-clearing refreshes. A zero-result fetch is removed from `Sections` and retried on the very next call rather than counting toward that "at most once," and a named section whose reload threw during a global refresh can remain stuck in `Sections` (marked loaded, no data) until the next global refresh — see step 7. The root section, by contrast, can be reloaded on every call that reaches it ([BR-EC-13](../business-rules.md#br-ec-13-each-section-is-loaded-from-the-environment-api-at-most-once-per-cache-generation)).
4. Otherwise, acquire `SectionsSemaphore` (double-checked locking: re-test `Sections.Contains(key)` after acquiring the lock, to avoid a redundant fetch from a concurrent caller) and call `LoadSettings(section)`/`LoadSettingsAsync(section)`.
5. `LoadSettings` builds the URL via `GetEnvironmentApiUrl`: if `key` case-insensitively equals `TrackName`, it calls `GET /v2/settings/{ApplicationName}/{ApplicationHost}/value`; otherwise it calls `GET /v2/settings/{ApplicationName}/{ApplicationHost}/{key}/value` ([BR-EC-15](../business-rules.md#br-ec-15-track-section-and-named-section-settings-use-different-url-shapes)). Because `GetConfigSetting` substitutes `TrackName` for a blank/omitted section before calling `LoadSection` (see [Get setting value](get-setting-value.md) step 1), the track-scoped bare URL is reached by passing the resolved track name, not by passing an empty string. When `section` is a literal empty string instead — the "root" case reached directly from [Has setting](has-setting.md)'s optional `section` parameter (which defaults to `""`), or from an explicit `""` passed to `GetSectionSettings(string section)`'s required argument — `key` is `""`, which does not equal `TrackName`, so the *named-section* branch fires with an empty segment, producing the double-slash URL `GET /v2/settings/{ApplicationName}/{ApplicationHost}//value`.
6. `HandleGetConfigurationSettingsResults` is called with `(key, result)`:
   - A `null` result, or a successful result whose body does not deserialize to a `ConfigurationSettings` with a non-null `Settings` dictionary, throws `EnvironmentClientException` ("Failed to get a result from {ServiceUrl}" / "Got a result, but no valid settings were returned from {ServiceUrl}") ([BR-EC-16](../business-rules.md#br-ec-16-a-null%2C-failed%2C-or-settings-less-environment-api-response-is-an-error)).
   - A non-successful HTTP result throws `EnvironmentClientException("Failed to load settings from {ServiceUrl}", innerException)`.
   - On success, each returned `ConfigSetting` is merged into the single global `Settings` `ConcurrentDictionary<string, ConfigSetting>` via `AddOrUpdate`, keyed `"{key}.{ConfigKey.Key}"` (or just `ConfigKey.Key` when `key` is empty — the literal-empty-string "root" case) — this is a per-key upsert into one shared dictionary, not a full per-section dictionary overwrite or atomic replace. Note that `key` here is already the resolved track name when the caller substituted it for a blank section, so a track-scoped load stores **prefixed** keys (`"{tracknamelower}.{ConfigKey.Key}"`), not bare ones.
   - If the deserialized `Settings` dictionary has **zero** entries, the section is treated as not-yet-loaded: it is **removed** from `Sections` if present (so it is retried on the next access) and the call returns without adding it to `Sections` — this is a deliberate self-healing fix for a 2026-07-14 incident where an empty response could otherwise permanently "stick" as loaded until a manual app-pool recycle ([BR-EC-17](../business-rules.md#br-ec-17-a-section-with-zero-returned-settings-is-retried-rather-than-marked-loaded)).
   - Otherwise, if `section` (the lowercased `key`) is **nonempty**, it is added to `Sections`, marking it as loaded for this cache generation. An empty/root section is **never** added to `Sections` — it has no "loaded" state to reach, so it is reloaded from the Environment API on every call that gets past step 3, not just once per cache generation.
7. A failure partway through the multi-section reload performed by [Refresh check and cache reload](refresh-check-and-cache-reload.md) throws before reaching this operation's own `Sections.Add` (step 6), but that reload runs `LoadSettings(section)` directly for each section already in `Sections` — it does not go through this operation's own `Sections.Contains(key)` guard (step 3). `Settings` is cleared globally before that reload loop runs, and `Sections` itself is never cleared, so if the per-section reload throws for a section that was already recorded in `Sections`, that section remains marked loaded with **no** corresponding entries in `Settings`. Because step 3 trusts `Sections` membership alone, this stuck named section is **not** retried on the next `LoadSection` call for it — it stays loaded-but-empty until the next global refresh clears and successfully reloads it again. This differs from a zero-result response (step 6), which actively removes the section from `Sections` and is retried immediately.

## Process flow

```mermaid
flowchart TD
    A["LoadSection section"] --> B[Refresh check and cache reload]
    B --> C["key = GetSectionKey(section)"]
    C --> D{"key already in Sections HashSet?"}
    D -- Yes --> E["Return: already loaded<br>(may be stale/empty if a prior<br>global reload threw — step 7)"]
    D -- No --> F["Acquire SectionsSemaphore, double-check"]
    F --> G{"key == TrackName (case-insensitive)?"}
    G -- Yes --> H["GET /v2/settings/app/host/value"]
    G -- No --> I["GET /v2/settings/app/host/key/value"]
    H --> J[HandleGetConfigurationSettingsResults]
    I --> J
    J -->|"null/failed/no Settings dict"| K[EnvironmentClientException]
    J -->|"success, zero entries"| L["Remove key from Sections, retry next time"]
    J -->|"success, 1+ entries"| M["AddOrUpdate each key.setting into global Settings"]
    M --> N{"key nonempty?"}
    N -- Yes --> O["Add key to Sections<br>(marked loaded)"]
    N -- No --> P["Root stays unmarked<br>(reloaded next call)"]
```

## Sources
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/BaseEnvironmentClient.cs`
- `src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/Data/ConfigurationSettings.cs`
