< Summary

Information
Class: AmbientServices.AmbientEnvironmentSettingsSet
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/AlternateImplementations/AmbientEnvironmentSettingsSet.cs
Tag: 332_35464845198
Line coverage
98%
Covered lines: 68
Uncovered lines: 1
Coverable lines: 69
Total lines: 205
Line coverage: 98.5%
Branch coverage
85%
Covered branches: 36
Total branches: 42
Branch coverage: 85.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor()100%22100%
ImportRegisteredEnvironmentVariables()100%44100%
NewSettingRegistered(...)100%44100%
Refresh()71.43%141493.75%
get_SetName()100%11100%
GetRawValue(...)100%22100%
GetTypedValue(...)75%88100%
get_SettingsAreMutable()100%11100%
ChangeSetting(...)100%11100%
InternalChangeSetting(...)100%88100%
ToString()100%11100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/AlternateImplementations/AmbientEnvironmentSettingsSet.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4
 5namespace AmbientServices;
 6
 7/// <summary>
 8/// A settings set that uses the process environment.
 9/// Note that since the framework does not provide an event for when environment variables change, changes after initial
 10/// </summary>
 11/// <remarks>
 12/// <pitch>A settings set backed by the process environment variables — the natural source for container- and CI-style c
 13/// <pledge><see cref="IAmbientSettingsSet"/></pledge>
 14/// <pledge>
 15/// Keys registered in <see cref="SettingsRegistry"/> (at construction or later) are snapshotted into memory; all other 
 16/// </pledge>
 17/// <plan>
 18/// Two <see cref="ConcurrentDictionary{TKey,TValue}"/> caches (raw and typed) hold only registered or explicitly-change
 19/// </plan>
 20/// <para><b>Security:</b> At construction time, only environment variables whose keys are already registered in <see cr
 21/// Settings registered later are imported when <see cref="SettingsRegistry.SettingRegistered"/> fires (see <c>NewSettin
 22/// Other variables are read lazily from the process environment when requested via <see cref="GetRawValue(string)"/> or
 23/// This reduces the risk that unrelated secrets in the environment (database passwords, API keys, tokens, etc.) are hel
 24/// <para><b>Security:</b> <see cref="ChangeSetting(string, string?)"/> calls <see cref="Environment.SetEnvironmentVaria
 25/// Callers should treat mutable environment settings as privileged operations.</para>
 26/// <para><b>Security:</b> Any value retrieved through this settings set may contain sensitive data. Avoid logging raw s
 27/// </remarks>
 28public class AmbientEnvironmentSettingsSet : IAmbientSettingsSet
 29{
 30    /// <summary>
 31    /// Gets the singleton instance.
 32    /// </summary>
 233    public static AmbientEnvironmentSettingsSet Instance { get; } = new();
 34
 35    private readonly LazyUnsubscribeWeakEventListenerProxy<AmbientEnvironmentSettingsSet, object?, IAmbientSettingInfo> 
 36    private readonly ConcurrentDictionary<string, string> _rawValues;
 37    private readonly ConcurrentDictionary<string, object> _typedValues;
 238    private readonly ConcurrentDictionary<string, byte> _observedKeys = new();
 239    private readonly object _lock = new();
 40
 41    /// <summary>
 42    /// Constructs the ambient environment settings set.
 43    /// </summary>
 244    internal AmbientEnvironmentSettingsSet()
 45    {
 246        _rawValues = new ConcurrentDictionary<string, string>();
 247        _typedValues = new ConcurrentDictionary<string, object>();
 248        ImportRegisteredEnvironmentVariables();
 249        _weakSettingRegistered = new LazyUnsubscribeWeakEventListenerProxy<AmbientEnvironmentSettingsSet, object?, IAmbi
 250                this, NewSettingRegistered, wvc => SettingsRegistry.DefaultRegistry.SettingRegistered -= wvc.WeakEventHa
 251        SettingsRegistry.DefaultRegistry.SettingRegistered += _weakSettingRegistered.WeakEventHandler;
 252    }
 53
 54    private void ImportRegisteredEnvironmentVariables()
 55    {
 256        foreach (IAmbientSettingInfo registered in SettingsRegistry.DefaultRegistry.Settings)
 57        {
 258            string? value = Environment.GetEnvironmentVariable(registered.Key);
 259            if (value == null) continue;
 260            _ = _rawValues.TryAdd(registered.Key, value);
 261            _typedValues[registered.Key] = registered.Convert(this, value);
 62        }
 263    }
 64
 65    private static void NewSettingRegistered(AmbientEnvironmentSettingsSet settingsSet, object? sender, IAmbientSettingI
 66    {
 267        string? value = settingsSet._rawValues.TryGetValue(setting.Key, out string? cached)
 268            ? cached
 269            : Environment.GetEnvironmentVariable(setting.Key);
 270        if (value != null)
 71        {
 272            _ = settingsSet._rawValues.TryAdd(setting.Key, value);
 273            settingsSet._typedValues[setting.Key] = setting.Convert(settingsSet, value);
 74        }
 275    }
 76    /// <summary>
 77    /// Refreshes the settings manually by re-reading the environment variables for registered keys, keys previously obs
 78    /// If another thread attempts to refresh while a refresh is happening, all threads will wait until all refreshes ar
 79    /// </summary>
 80    public void Refresh()
 81    {
 282        lock (_lock)    // this maybe could be improved, but we need to ensure that the first entry to this loop gets pr
 83        {
 284            HashSet<string> keysToSync = new(StringComparer.Ordinal);
 285            foreach (string key in _rawValues.Keys)
 86            {
 087                keysToSync.Add(key);
 88            }
 289            foreach (string key in _observedKeys.Keys)
 90            {
 291                keysToSync.Add(key);
 92            }
 293            foreach (IAmbientSettingInfo registered in SettingsRegistry.DefaultRegistry.Settings)
 94            {
 295                keysToSync.Add(registered.Key);
 96            }
 97
 298            Dictionary<string, string?> updates = new();
 299            foreach (string key in keysToSync)
 100            {
 2101                string? envValue = Environment.GetEnvironmentVariable(key);
 2102                if (!_rawValues.TryGetValue(key, out string? cached) || !string.Equals(cached, envValue, StringCompariso
 103                {
 2104                    updates[key] = envValue;
 105                }
 106            }
 107
 2108            foreach (KeyValuePair<string, string?> kvp in updates)
 109            {
 2110                _ = InternalChangeSetting(kvp.Key, kvp.Value);
 111            }
 112        }
 2113    }
 114
 115    /// <summary>
 116    /// Gets the name of the settings set so that a settings consumer can know where a changed setting value came from.
 117    /// </summary>
 2118    public string SetName => "Environment";
 119    /// <summary>
 120    /// Gets the current raw (string) value for the specified key, or null if the setting is not set.
 121    /// Values changed in the environment after initialization are visible here without calling <see cref="Refresh"/> un
 122    /// </summary>
 123    /// <param name="key">A key identifying the setting whose value is to be retrieved.</param>
 124    /// <returns>The setting value, or null if the setting is not set.</returns>
 125    public string? GetRawValue(string key)
 126    {
 2127        _ = _observedKeys.TryAdd(key, 0);
 2128        if (_rawValues.TryGetValue(key, out string? cached))
 129        {
 2130            return cached;
 131        }
 2132        return Environment.GetEnvironmentVariable(key);
 133    }
 134    /// <summary>
 135    /// Gets the current typed value for the setting with the specified key, or null if the setting is not set.
 136    /// </summary>
 137    /// <param name="key">A key identifying the setting whose value is to be retrieved.</param>
 138    /// <returns>The setting value, or null if the setting is not set.</returns>
 139    public object? GetTypedValue(string key)
 140    {
 2141        _ = _observedKeys.TryAdd(key, 0);
 2142        if (_typedValues.TryGetValue(key, out object? typed))
 143        {
 2144            return typed;
 145        }
 2146        string? raw = _rawValues.TryGetValue(key, out string? cached) ? cached : Environment.GetEnvironmentVariable(key)
 2147        if (raw == null) return null;
 2148        IAmbientSettingInfo? ps = SettingsRegistry.DefaultRegistry.TryGetSetting(key);
 2149        return (ps != null) ? ps.Convert(this, raw) : raw;
 150    }
 151
 152    /// <summary>
 153    /// Gets whether or not the settings set is mutable.
 154    /// </summary>
 2155    public bool SettingsAreMutable => true;
 156
 157    /// <summary>
 158    /// Changes the specified setting in the process environment and in this settings set's in-memory caches.
 159    /// </summary>
 160    /// <remarks>
 161    /// <b>Security:</b> This updates the process environment via <see cref="Environment.SetEnvironmentVariable(string, 
 162    /// Use only for settings that are safe to propagate at the process level.
 163    /// </remarks>
 164    /// <param name="key">A string that uniquely identifies the setting.</param>
 165    /// <param name="value">The new string value for the setting, or null if the setting should be removed.</param>
 166    /// <returns>Whether or not the setting actually changed.</returns>
 167    public bool ChangeSetting(string key, string? value)
 168    {
 2169        Environment.SetEnvironmentVariable(key, value);
 2170        bool ret = InternalChangeSetting(key, value);
 2171        return ret;
 172    }
 173    private bool InternalChangeSetting(string key, string? value)
 174    {
 2175        _ = _observedKeys.TryAdd(key, 0);
 2176        if (value == null)
 177        {
 2178            _ = _rawValues.TryRemove(key, out string? oldValue);
 2179            _ = _typedValues.TryRemove(key, out _);
 180            // did the value *not* change?  return that fact
 2181            if (oldValue == null) return false;
 182        }
 183        else
 184        {
 2185            string? oldValue = null;
 2186            _ = _rawValues.AddOrUpdate(key, value, (k, v) => { System.Threading.Interlocked.CompareExchange(ref oldValue
 187            // did the value *not* change?
 2188            if (string.Equals(value, oldValue, StringComparison.Ordinal))
 189            {
 2190                return false;
 191            }
 2192            IAmbientSettingInfo? ps = SettingsRegistry.DefaultRegistry.TryGetSetting(key);
 2193            _typedValues[key] = (ps != null) ? ps.Convert(this, value) : value;
 194        }
 2195        return true;
 196    }
 197    /// <summary>
 198    /// Gets a string representing the settings instance.
 199    /// </summary>
 200    /// <returns></returns>
 201    public override string ToString()
 202    {
 2203        return "Settings: Environment";
 204    }
 205}