Skip to content

Load section

Field Value
Owner LoadSection/LoadSectionAsync → LoadSettings[Async] → HandleGetConfigurationSettingsResults → StoreSettingInDictionary
Trigger Called internally by Get setting value, Has setting, and Get section settings
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'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 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.
  2. 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.
  3. 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.
  4. Origin: Steps 1.1–1.2 are implemented in Refresh check and cache reload and executed on behalf of this operation.

  5. Compute key = GetSectionKey(section) (section.ToLower(), with no null-guard — a null section throws NullReferenceException here rather than aliasing to root).
  6. 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).
  7. 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).
  8. 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). Because GetConfigSetting substitutes TrackName for a blank/omitted section before calling LoadSection (see Get setting value 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'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.
  9. HandleGetConfigurationSettingsResults is called with (key, result):
  10. 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).
  11. A non-successful HTTP result throws EnvironmentClientException("Failed to load settings from {ServiceUrl}", innerException).
  12. 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.
  13. 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).
  14. 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.
  15. A failure partway through the multi-section reload performed by Refresh check and cache reload 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

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