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¶
- Call
CheckForUpdates()unconditionally — everyLoadSectioncall checks whether a global cache refresh is due, regardless of whether this particular section is already loaded. - 1.1. An empty global
Settingscache (including the very first call) always triggers a reload with no server probe, and in that caseLastCheckForRefreshis 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, updatesLastCheckForRefresh = DateTime.Now, and triggers the reload only if the returned server timestamp is later than the client's currentLastRefreshed— a due probe does not itself guarantee a reload. - 1.2. A triggered reload acquires
SectionsSemaphore, setsLastRefreshed = DateTime.Nowbefore any loading happens (not after a successful reload completes), clears the entire globalSettingsdictionary, then reloads the literal-empty-string root section plus every section previously recorded inSections— this reload is not atomic, so a failure partway through can leave the cache empty or only partially repopulated.Sectionsitself is never cleared by this reload (onlySettingsis), so a named section that was already recorded inSectionsbefore the refresh stays recorded even if its individual reload throws; see step 7. Only a nonempty section name reachingHandleGetConfigurationSettingsResultswith 1+ returned settings is (re-)added toSectionsand 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 and executed on behalf of this operation.
- Compute
key = GetSectionKey(section)(section.ToLower(), with no null-guard — anullsection throwsNullReferenceExceptionhere rather than aliasing to root). - If
keyis already present in theSectionsHashSet<string>, return immediately without a network call. The root/empty section (key == "") is never added toSections(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 fromSectionsand 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 inSections(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). - Otherwise, acquire
SectionsSemaphore(double-checked locking: re-testSections.Contains(key)after acquiring the lock, to avoid a redundant fetch from a concurrent caller) and callLoadSettings(section)/LoadSettingsAsync(section). LoadSettingsbuilds the URL viaGetEnvironmentApiUrl: ifkeycase-insensitively equalsTrackName, it callsGET /v2/settings/{ApplicationName}/{ApplicationHost}/value; otherwise it callsGET /v2/settings/{ApplicationName}/{ApplicationHost}/{key}/value(BR-EC-15). BecauseGetConfigSettingsubstitutesTrackNamefor a blank/omitted section before callingLoadSection(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. Whensectionis a literal empty string instead — the "root" case reached directly from Has setting's optionalsectionparameter (which defaults to""), or from an explicit""passed toGetSectionSettings(string section)'s required argument —keyis"", which does not equalTrackName, so the named-section branch fires with an empty segment, producing the double-slash URLGET /v2/settings/{ApplicationName}/{ApplicationHost}//value.HandleGetConfigurationSettingsResultsis called with(key, result):- A
nullresult, or a successful result whose body does not deserialize to aConfigurationSettingswith a non-nullSettingsdictionary, throwsEnvironmentClientException("Failed to get a result from {ServiceUrl}" / "Got a result, but no valid settings were returned from {ServiceUrl}") (BR-EC-16). - A non-successful HTTP result throws
EnvironmentClientException("Failed to load settings from {ServiceUrl}", innerException). - On success, each returned
ConfigSettingis merged into the single globalSettingsConcurrentDictionary<string, ConfigSetting>viaAddOrUpdate, keyed"{key}.{ConfigKey.Key}"(or justConfigKey.Keywhenkeyis 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 thatkeyhere 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
Settingsdictionary has zero entries, the section is treated as not-yet-loaded: it is removed fromSectionsif present (so it is retried on the next access) and the call returns without adding it toSections— 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). - Otherwise, if
section(the lowercasedkey) is nonempty, it is added toSections, marking it as loaded for this cache generation. An empty/root section is never added toSections— 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. - 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 runsLoadSettings(section)directly for each section already inSections— it does not go through this operation's ownSections.Contains(key)guard (step 3).Settingsis cleared globally before that reload loop runs, andSectionsitself is never cleared, so if the per-section reload throws for a section that was already recorded inSections, that section remains marked loaded with no corresponding entries inSettings. Because step 3 trustsSectionsmembership alone, this stuck named section is not retried on the nextLoadSectioncall 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 fromSectionsand 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.cssrc/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/Data/ConfigurationSettings.cs