Skip to content

SAIF.NetCore.EnvironmentClient architecture

SAIF.NetCore.EnvironmentClient is the implementation-layer library that provides BaseEnvironmentClient and SecureEnvironmentClient, the concrete implementations of the IEnvironmentClient contract from SAIF.NetCore.ServiceContracts; it resolves which environment "track" the running application belongs to, then lazily downloads, caches, and refreshes named configuration settings — and, for the secure variant, crypto-protected settings — from a central Environment API over HTTP.

Responsibilities

  • Implement IEnvironmentClient and expose configuration-value accessors (GetSettingValue/GetSettingValueAsync with multiple overloads, GetSectionSettings, HasSetting) and secure-setting accessors (GetSecureSetting/GetSecureSettingAsync).
  • Resolve the application's environment track and host identity at startup (Setup) via GetHostsByTrack and IHostResolver.ResolveHost, and support a developer-machine configuration override file.
  • Lazily load and cache configuration sections and settings from the Environment API, with a time-based refresh check (TestForRefreshSettings, throttled to roughly 60 seconds).
  • Gate access to secure (crypto) settings behind a system hash (ISystemHashService), implemented in SecureEnvironmentClient, which throws in the base class.
  • Resolve the current host's Active Directory group membership (HostResolver) to decide trust/override behavior.

Component boundaries

  • BaseEnvironmentClient: the primary IEnvironmentClient implementation; owns configuration/section caching (ConcurrentDictionary, HashSet of loaded sections, SemaphoreSlim), host/track resolution, the developer override file, and all non-secure setting access. RetrieveSecureSetting<T>/RetrieveSecureSettingAsync<T> are virtual and throw "Operation not allowed!" by default*.
  • SecureEnvironmentClient: inherits BaseEnvironmentClient and overrides the secure-setting retrieval methods, gated by a constructor check (COREAPIENABLED=1 or developer machine) and backed by SecureSettingCache.
  • SecureSettingCache: an in-memory Get/GetAsync cache keyed by setting key and last-refreshed timestamp, with a ClearCache method that is never invoked by this library*.
  • HostResolver / IHostResolver: resolves the current host from a candidate list using Active Directory group membership (System.DirectoryServices.AccountManagement).
  • IEnvironmentClient, IHttpJsonClient, IHttpRequestResult, ISystemHashService (ServiceContracts interfaces): consumed, not implemented beyond IEnvironmentClient; the HTTP transport and system-hash computation are supplied by whatever the host application registers (in the standard stack, SAIF.NetCore.HttpClients and SAIF.NetCore.SystemHash/SAIF.NetCore.HashUtil respectively)*.

Interaction model

  • A consuming application (typically via SAIF.NetCore.ServiceExtensions) constructs BaseEnvironmentClient or SecureEnvironmentClient with IConfiguration, IHostEnvironment, IHttpJsonClient, IHostResolver, and ISystemHashService. The constructor itself calls Setup() — setup is not a separate step the caller invokes; it runs synchronously during construction and resolves the application host and environment track against the Environment API before the constructor returns.
  • Callers then invoke GetSettingValue/GetSettingValueAsync (optionally scoped to a section) or, on SecureEnvironmentClient, GetSecureSetting/GetSecureSettingAsync; both paths unconditionally run the refresh check (CheckForUpdates) first, then lazily load the target section from the Environment API if it has not already been loaded in the current cache generation.
  • All outbound calls are issued through the injected IHttpJsonClient against the base URL supplied by the EnvironmentApiUrl OS environment variable; secure-setting calls additionally append a systemHashValue query parameter computed via ISystemHashService.Get(SystemType).
  • A local developer-settings override file (resolved by HostResolver's Active Directory check) is consulted only after the non-secure setting path has already triggered the refresh-check/load sequence described above — it changes which value is returned once loading has happened, it does not let a developer machine skip the Environment API bootstrap or load/refresh network dependency. The override file is never consulted for secure settings, HasSetting, or GetSectionSettings.

Architecture diagram

The following diagram shows how BaseEnvironmentClient/SecureEnvironmentClient sit between a consuming application and the remote Environment API, mediated by the ServiceContracts interfaces they depend on.

flowchart TD
    APP["Consuming application<br>(via SAIF.NetCore.ServiceExtensions)"] -->|constructs + calls| BEC["BaseEnvironmentClient"]
    BEC --> SEC["SecureEnvironmentClient<br>(inherits BaseEnvironmentClient)"]
    SEC --> CACHE["SecureSettingCache"]
    BEC -->|SetClientInfo / Get / GetAsync| HTTP["IHttpJsonClient<br>(ServiceContracts contract)"]
    SEC -->|"Get(SystemType)"| HASH["ISystemHashService<br>(ServiceContracts contract)"]
    HTTP -->|GET tracks/settings/secureSetting endpoints| API["Environment API<br>(external HTTP service)"]
    BEC -->|AD group lookup| RES["IHostResolver / HostResolver"]

Sequence diagram

The following diagram shows construction-time setup followed by the primary request/response sequence for resolving a non-secure configuration setting, including the server-timestamp refresh probe, the empty-cache reload path, and section loading.

sequenceDiagram
    participant Consumer
    participant BaseEnvironmentClient
    participant IHttpJsonClient
    participant EnvironmentAPI
    Consumer->>BaseEnvironmentClient: new BaseEnvironmentClient(...)
    activate BaseEnvironmentClient
    BaseEnvironmentClient->>BaseEnvironmentClient: Setup() — resolve host/track, GET tracks endpoint
    BaseEnvironmentClient->>IHttpJsonClient: Get("/v2/tracks/{app}/value")
    IHttpJsonClient->>EnvironmentAPI: GET tracks
    EnvironmentAPI-->>IHttpJsonClient: wire response
    IHttpJsonClient-->>BaseEnvironmentClient: deserialized result
    deactivate BaseEnvironmentClient
    Consumer->>BaseEnvironmentClient: GetSettingValue(key, section)
    BaseEnvironmentClient->>BaseEnvironmentClient: CheckForUpdates (always runs first)
    alt Settings dictionary empty
        BaseEnvironmentClient->>BaseEnvironmentClient: NeedsRefresh = true (no probe call)
    else at least 60s since last probe
        BaseEnvironmentClient->>IHttpJsonClient: Get("/settings/lastRefreshed")
        IHttpJsonClient->>EnvironmentAPI: GET lastRefreshed
        EnvironmentAPI-->>IHttpJsonClient: wire response
        IHttpJsonClient-->>BaseEnvironmentClient: server timestamp
        BaseEnvironmentClient->>BaseEnvironmentClient: NeedsRefresh = server timestamp > LastRefreshed
    else probe not yet due
        BaseEnvironmentClient->>BaseEnvironmentClient: NeedsRefresh = false
    end
    alt NeedsRefresh = true
        BaseEnvironmentClient->>BaseEnvironmentClient: LastRefreshed = now, Settings.Clear(), reload root + known sections
    end
    alt section not yet loaded this cache generation
        BaseEnvironmentClient->>IHttpJsonClient: Get("/v2/settings/{app}/{host}/{section}/value")
        IHttpJsonClient->>EnvironmentAPI: GET settings endpoint
        EnvironmentAPI-->>IHttpJsonClient: wire response
        IHttpJsonClient-->>BaseEnvironmentClient: deserialized ConfigurationSettings
        BaseEnvironmentClient->>BaseEnvironmentClient: AddOrUpdate each setting into global Settings dictionary
    end
    BaseEnvironmentClient->>BaseEnvironmentClient: Developer override lookup (checked after the load above, not instead of it)
    BaseEnvironmentClient-->>Consumer: setting value

Design constraints

  • No project references between libraries in this repository; the only cross-library dependency is the SAIF.NetCore.ServiceContracts NuGet package, consistent with the repository-wide layering in Architecture.md.
  • SecureEnvironmentClient gates secure-setting access behind COREAPIENABLED=1 or developer-machine detection; the base class unconditionally throws for secure-setting retrieval, so secure access requires the derived type to be registered*.
  • LastRefreshed is tracked using client local time but compared against the server's reported lastRefreshed, so clock skew between client and server could cause missed or extra refreshes*.
  • The Sections HashSet is read outside its guarding semaphore in at least one path while also being mutated during reloads, which is a potential thread-safety gap*.
  • Server-side Environment API semantics (hash validation, lastRefreshed meaning, response casing) are outside this repository and cannot be verified from source alone**.

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/SecureSettingCache.cs
  • src/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/SAIF.NetCore.EnvironmentClient/HostResolver.cs
  • src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/IEnvironmentClient.cs, ISystemHashService.cs, IHttpJsonClient.cs, IHttpRequestResult.cs
  • Architecture.md