< Summary

Information
Class: AmbientServices.SettingsRegistry
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/SettingsHelpers.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 33
Uncovered lines: 0
Coverable lines: 33
Total lines: 712
Line coverage: 100%
Branch coverage
100%
Covered branches: 26
Total branches: 26
Branch coverage: 100%
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%11100%
.ctor()100%11100%
Register(...)100%1818100%
get_Settings()100%44100%
TryGetSetting(...)100%44100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/SettingsHelpers.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4
 5namespace AmbientServices;
 6
 7/// <summary>
 8/// Event arguments for a failed ambient setting string-to-typed conversion.
 9/// </summary>
 10/// <param name="key">The setting key.</param>
 11/// <param name="rawValue">The raw string value that could not be converted.</param>
 12/// <param name="exception">The exception thrown by the conversion delegate.</param>
 13/// <param name="defaultValue">The default value that will be used instead.</param>
 14public class SettingConversionFailedEventArgs(string key, string rawValue, Exception exception, object? defaultValue) : 
 15{
 16    /// <summary>The setting key.</summary>
 17    public string Key { get; } = key;
 18    /// <summary>The raw string value that could not be converted.</summary>
 19    public string RawValue { get; } = rawValue;
 20    /// <summary>The exception thrown by the conversion delegate.</summary>
 21    public Exception Exception { get; } = exception;
 22    /// <summary>The default value that will be used instead.</summary>
 23    public object? DefaultValue { get; } = defaultValue;
 24}
 25
 26/// <summary>
 27/// A static factory class for declaring strongly-typed <see cref="IAmbientSetting{T}"/> settings and registering them f
 28/// </summary>
 29/// <remarks>
 30/// <pitch>The factory callers use to declare typed settings: give it a key, description, conversion, and default and ge
 31/// <pledge>
 32/// Every factory call registers the setting's key, description, and default in the process-wide <see cref="SettingsRegi
 33/// </pledge>
 34/// <priority>
 35/// 1. A setting that always reads over surfacing a bad value: a missing setting yields the declared default and a faile
 36/// 2. Process-wide discoverability over a free declaration: every factory call registers the key, description, and defa
 37/// </priority>
 38/// </remarks>
 39public static class AmbientSettings
 40{
 41    /// <summary>
 42    /// Raised when a registered setting's conversion delegate throws. The default value is used instead.
 43    /// Subscribers must not call back into ambient logging or settings retrieval for the same key, as that may recurse.
 44    /// </summary>
 45    public static event EventHandler<SettingConversionFailedEventArgs>? ConversionFailed;
 46
 47    internal static void NotifyConversionFailed(string key, string rawValue, Exception exception, object? defaultValue)
 48    {
 49        ConversionFailed?.Invoke(null, new SettingConversionFailedEventArgs(key, rawValue, exception, defaultValue));
 50    }
 51    /// <summary>
 52    /// Gets an enumeration of <see cref="IAmbientSettingInfo"/> with descriptions and the last time used for all ambien
 53    /// </summary>
 54    public static IEnumerable<IAmbientSettingInfo> AmbientSettingsInfo => SettingsRegistry.DefaultRegistry.Settings;
 55    /// <summary>
 56    /// Construct a setting instance.
 57    /// If a non-null settings set is specified, the setting will be attached to that, otherwise the ambient settings se
 58    /// Never returns a setting whose value is always the default value.
 59    /// </summary>
 60    /// <param name="settingsSet">The <see cref="IAmbientSettingsSet"/> to get the setting value from.  If null, returns
 61    /// <param name="key">A key string identifying the setting.</param>
 62    /// <param name="description">A description of the setting.</param>
 63    /// <param name="convert">A delegate that takes a string and returns the type.</param>
 64    /// <param name="defaultValueString">The default string value for the setting.  This will be converted for the curre
 65    public static IAmbientSetting<T> GetSetting<T>(IAmbientSettingsSet? settingsSet, string key, string description, Fun
 66    {
 67        return (settingsSet == null) ? GetAmbientSetting<T>(key, description, convert, defaultValueString) : GetSettings
 68    }
 69    /// <summary>
 70    /// Construct a setting instance.
 71    /// If a non-null settings set is specified, the setting will be attached to that, otherwise the ambient settings se
 72    /// Never returns a setting whose value is always the default value.
 73    /// </summary>
 74    /// <param name="settingsSet">The <see cref="IAmbientSettingsSet"/> to get the setting value from.  If null, returns
 75    /// <param name="key">A key string identifying the setting.</param>
 76    /// <param name="description">A description of the setting.</param>
 77    /// <param name="defaultValue">The default value for the setting.  This will be used as the current value if the set
 78    /// <param name="convert">A delegate that takes a string and returns the type.</param>
 79    public static IAmbientSetting<T> GetSetting<T>(IAmbientSettingsSet? settingsSet, string key, string description, T d
 80    {
 81        return (settingsSet == null) ? GetAmbientSetting<T>(key, description, defaultValue, convert) : GetSettingsSetSet
 82    }
 83    /// <summary>
 84    /// Construct a setting instance that uses a specific settings set and caches the setting with the specified key, co
 85    /// </summary>
 86    /// <param name="settingsSet">The <see cref="IAmbientSettingsSet"/> to get the setting value from.  If null, the set
 87    /// <param name="key">A key string identifying the setting.</param>
 88    /// <param name="description">A description of the setting.</param>
 89    /// <param name="convert">A delegate that takes a string and returns the type.</param>
 90    /// <param name="defaultValueString">The default string value for the setting.  This will be converted for the curre
 91    public static IAmbientSetting<T> GetSettingsSetSetting<T>(IAmbientSettingsSet? settingsSet, string key, string descr
 92    {
 93        return new SettingsSetSetting<T>(settingsSet, key, description, convert, defaultValueString);
 94    }
 95    /// <summary>
 96    /// Construct a setting instance that uses a specific settings set and caches the setting with the specified key, co
 97    /// </summary>
 98    /// <param name="settingsSet">The <see cref="IAmbientSettingsSet"/> to get the setting value from.  If null, the set
 99    /// <param name="key">A key string identifying the setting.</param>
 100    /// <param name="description">A description of the setting.</param>
 101    /// <param name="defaultValue">The default value for the setting.  This will be used as the current value if the set
 102    /// <param name="convert">A delegate that takes a string and returns the type.</param>
 103    public static IAmbientSetting<T> GetSettingsSetSetting<T>(IAmbientSettingsSet? settingsSet, string key, string descr
 104    {
 105        return new SettingsSetSetting<T>(settingsSet, key, description, defaultValue, convert);
 106    }
 107    /// <summary>
 108    /// Construct an ambient setting instance that caches the setting with the specified key, converting it from a strin
 109    /// Settings will be gathered from the ambient local settings set, if one exists.
 110    /// </summary>
 111    /// <param name="key">A key string identifying the setting.</param>
 112    /// <param name="description">A description of the setting.</param>
 113    /// <param name="convert">A delegate that takes a string and returns the type.</param>
 114    /// <param name="defaultValueString">The default string value for the setting.  This will be converted for the curre
 115    public static IAmbientSetting<T> GetAmbientSetting<T>(string key, string description, Func<string, T> convert, strin
 116    {
 117        return new AmbientSetting<T>(key, description, convert, defaultValueString);
 118    }
 119    /// <summary>
 120    /// Construct an ambient setting instance that caches the setting with the specified key, converting it from a strin
 121    /// Settings will be gathered from the ambient local settings set, if one exists.
 122    /// </summary>
 123    /// <param name="key">A key string identifying the setting.</param>
 124    /// <param name="description">A description of the setting.</param>
 125    /// <param name="defaultValue">The default value for the setting.  This will be used as the current value if the set
 126    /// <param name="convert">A delegate that takes a string and returns the type.</param>
 127    public static IAmbientSetting<T> GetAmbientSetting<T>(string key, string description, T defaultValue, Func<string, T
 128    {
 129        return new AmbientSetting<T>(key, description, defaultValue, convert);
 130    }
 131}
 132
 133/// <summary>
 134/// An abstraction of a setting that holds a strongly-typed value read from the ambient settings.
 135/// </summary>
 136/// <remarks>
 137/// <pitch>A strongly-typed handle to one setting: read it at any time and get the current, already-converted value with
 138/// <pledge>Reading the value never fails and is cheap enough for hot paths — conversion results are cached, so reads do
 139/// </remarks>
 140/// <typeparam name="T">The type represented by the the setting.</typeparam>
 141public interface IAmbientSetting<T>
 142{
 143    /// <summary>
 144    /// Gets the key of the setting, which may be useful in providing an override without having to match the name with 
 145    /// </summary>
 146    string Key { get; }
 147    /// <summary>
 148    /// Gets the current value of the setting (cached from the value given by the settings set).
 149    /// </summary>
 150    T Value { get; }
 151    /// <summary>
 152    /// Gets the current value of the setting along with the name of the set that the value came from (or null if the de
 153    /// </summary>
 154    /// <returns>The current value of the setting along with the name of the set that the value came from (or null if th
 155    (T, string) GetValueWithSetName();
 156}
 157/// <summary>
 158/// An abstraction that provides access to a setting's description and last time used.
 159/// </summary>
 160/// <remarks>
 161/// <pitch>The declaration-side view of a registered setting — its key, description, default, string-to-typed conversion
 162/// <pledge>Conversion is called inline by settings sets whenever a raw value needs typing, so it must always return a u
 163/// </remarks>
 164public interface IAmbientSettingInfo
 165{
 166    /// <summary>
 167    /// Gets the setting key.
 168    /// </summary>
 169    string Key { get; }
 170    /// <summary>
 171    /// Gets the default value for the setting.
 172    /// </summary>
 173    string DefaultValueString { get; }
 174    /// <summary>
 175    /// Gets a description of the setting.
 176    /// </summary>
 177    string Description { get; }
 178    /// <summary>
 179    /// Gets the last time the setting's value was retrieved.
 180    /// </summary>
 181    DateTime LastUsed { get; }
 182    /// <summary>
 183    /// Gets the default value that is used when the value is not found in the settings set.
 184    /// </summary>
 185    object? DefaultValue { get; }
 186    /// <summary>
 187    /// Converts the setting value from the specified string to a typed value.
 188    /// The implementor may cache the value being generated if the settings set is the global settings set.
 189    /// </summary>
 190    /// <param name="settingsSet">The settings set asking for the setting value.</param>
 191    /// <param name="value">The typed value for the setting.</param>
 192    /// <returns>The typed value for the setting.</returns>
 193    object Convert(IAmbientSettingsSet settingsSet, string value);
 194}
 195/// <summary>
 196/// A global registry for settings that includes their keys, descriptions, last used time, conversion functions, and def
 197/// </summary>
 198/// <remarks>
 199/// <pitch>The process-wide directory of every setting declared anywhere in the process: settings sets consult it to con
 200/// <pledge>Registering the same key again is harmless when the description and default match and an error when they con
 201/// <plan>A <see cref="ConcurrentDictionary{TKey,TValue}"/> maps each key to a <see cref="WeakReference{T}"/> to the set
 202/// </remarks>
 203public class SettingsRegistry
 204{
 205    /// <summary>
 206    /// Gets the default registry.
 207    /// </summary>
 3208    public static SettingsRegistry DefaultRegistry { get; } = new();
 209
 210    private readonly int _registerLoopLimit;
 3211    private readonly ConcurrentDictionary<string, WeakReference<IAmbientSettingInfo>> _settings = new();
 212
 213    /// <summary>
 214    /// Constructs a registry with the default register retry limit.
 215    /// </summary>
 3216    public SettingsRegistry() : this(10)
 217    {
 3218    }
 219
 220    /// <summary>
 221    /// Constructs a registry (used by tests to exercise rare contention paths with a low retry limit).
 222    /// </summary>
 223    /// <param name="registerLoopLimit">Maximum register retry iterations before timing out.</param>
 3224    internal SettingsRegistry(int registerLoopLimit)
 225    {
 3226        _registerLoopLimit = registerLoopLimit;
 3227    }
 228    /// <summary>
 229    /// Registers an <see cref="IAmbientSettingInfo"/> in the global registry.
 230    /// </summary>
 231    /// <param name="setting">The <see cref="IAmbientSettingInfo"/> to register.</param>
 232    public void Register(IAmbientSettingInfo setting)
 233    {
 234#if NET5_0_OR_GREATER
 3235        ArgumentNullException.ThrowIfNull(setting);
 236#else
 237        if (setting is null) throw new ArgumentNullException(nameof(setting));
 238#endif
 3239        WeakReference<IAmbientSettingInfo> newReference = new(setting);
 240        WeakReference<IAmbientSettingInfo> existingReference;
 3241        int loopCount = 0;
 242        do
 243        {
 244            IAmbientSettingInfo? existingSetting;
 3245            existingReference = _settings.GetOrAdd(setting.Key, newReference);
 246            // did we NOT succeed?
 3247            if (newReference != existingReference)
 248            {
 249                // is the old one gone?
 2250                if (!existingReference.TryGetTarget(out existingSetting))
 251                {
 252                    // overwrite that one--were we NOT able to overwrite it?
 2253                    if (!_settings.TryUpdate(setting.Key, newReference, existingReference))
 254                    { // Coverage note: the inside of this loop is nearly impossible to cover in tests
 255                        // wait a bit and try again
 2256                        System.Threading.Thread.Sleep((int)Math.Pow(2, loopCount + 1));
 2257                        continue;
 258                    } // else we successfully overwrote it and there is no need to look for a conflict
 2259                    existingSetting = setting;
 260                }
 261                // the old one is still there, so we need to check for a conflict
 2262                string existingDescription = existingSetting.Description ?? "<null>";
 2263                string description = setting.Description ?? "<null>";
 2264                string existingDefaultValueString = existingSetting.DefaultValueString;
 2265                string defaultValueString = setting.DefaultValueString;
 2266                if (!string.Equals(existingDescription, description, StringComparison.Ordinal) || !string.Equals(existin
 267                {
 2268                    throw new ArgumentException($"A setting with the key {setting.Key} has already been registered ({exi
 269                } // else we didn't succeed, but it doesn't look like there is a conflict anyway, so we're probably fine
 270            } // else we succeeded
 271            // raise the registered event
 3272            SettingRegistered?.Invoke(null, setting);
 3273            return;
 274            // Coverage note: the loop and exception is nearly impossible to cover in tests
 2275        } while (loopCount++ < _registerLoopLimit);
 2276        throw new TimeoutException("Timeout attempting to register setting!");
 277    }
 278    /// <summary>
 279    /// Gets an enumeration of all the settings in the system.
 280    /// </summary>
 281    public IEnumerable<IAmbientSettingInfo> Settings
 282    {
 283        get
 284        {
 2285            foreach (KeyValuePair<string, WeakReference<IAmbientSettingInfo>> s in _settings)
 286            {
 287                IAmbientSettingInfo? setting;
 2288                if (s.Value.TryGetTarget(out setting))
 289                {
 2290                    yield return setting;
 291                }
 292                else
 293                {
 2294                    _settings.TryRemove(s.Key, out _);
 295                }
 296            }
 2297        }
 298    }
 299    /// <summary>
 300    /// Attempts to get the <see cref="IAmbientSettingInfo"/> for the specified settings key.
 301    /// </summary>
 302    /// <param name="key">The key for the setting.</param>
 303    /// <returns>An <see cref="IAmbientSettingInfo"/> for the specified setting.</returns>
 304    public IAmbientSettingInfo? TryGetSetting(string key)
 305    {
 306        WeakReference<IAmbientSettingInfo>? wrSetting;
 307        IAmbientSettingInfo? setting;
 3308        return _settings.TryGetValue(key, out wrSetting) ? (wrSetting.TryGetTarget(out setting) ? setting : null) : null
 309    }
 310    /// <summary>
 311    /// An event the is raised when a new setting is registered, allowing settings sets to call the setting's conversion
 312    /// </summary>
 313#pragma warning disable CA1003  // this event is performance critical
 314    public event EventHandler<IAmbientSettingInfo>? SettingRegistered;
 315#pragma warning restore CA1003
 316}
 317
 318/// <summary>
 319/// An immutable class that contains a typed setting value and the settings set it came from.
 320/// </summary>
 321/// <typeparam name="T">The type for the setting.</typeparam>
 322internal class SettingsSetSettingValue<T>
 323{
 324    public T Value { get; private set; }
 325    public IAmbientSettingsSet SettingsSet { get; private set; }
 326
 327    public SettingsSetSettingValue(T value, IAmbientSettingsSet settingsSet)
 328    {
 329        Value = value;
 330        SettingsSet = settingsSet;
 331    }
 332}
 333/// <summary>
 334/// The <see cref="IAmbientSettingsSet"/> that is attached to default values.
 335/// Note that this settings set does NOT actually contain any settings, it returns null for all keys.
 336/// Default values are stored with their individual <see cref="IAmbientSettingInfo"/> instance.
 337/// </summary>
 338/// <remarks>
 339/// <pitch>The provenance marker for default values: a singleton, deliberately empty settings set whose name is reported
 340/// <pledge><see cref="IAmbientSettingsSet"/></pledge>
 341/// <pledge>Returns null for every key and is immutable (changing a setting throws <see cref="InvalidOperationException"
 342/// <plan>A stateless singleton; every lookup returns null unconditionally.</plan>
 343/// </remarks>
 344public class DefaultSettingsSet : IAmbientSettingsSet
 345{
 346    /// <summary>
 347    /// Gets the singleton instance of <see cref="DefaultSettingsSet"/>.
 348    /// </summary>
 349    public static DefaultSettingsSet Instance { get; } = new();
 350
 351    private DefaultSettingsSet()
 352    {
 353    }
 354    /// <summary>
 355    /// Gets the name of the set of settings so that a settings consumer can know where a changed setting value came fro
 356    /// </summary>
 357    public string SetName => "DefaultSettingsValues";
 358    /// <summary>
 359    /// Gets the current raw value for the setting with the specified key, or null if the setting is not set.
 360    /// </summary>
 361    /// <param name="key">A key identifying the setting whose value is to be retrieved.</param>
 362    /// <returns>The setting value, or null if the setting is not set.</returns>
 363    public string? GetRawValue(string key)
 364    {
 365        return null;
 366    }
 367    /// <summary>
 368    /// Gets the current typed value for the setting with the specified key, or null if the setting is not set.
 369    /// </summary>
 370    /// <param name="key">A key identifying the setting whose value is to be retrieved.</param>
 371    /// <returns>The setting value, or null if the setting is not set.</returns>
 372    public object? GetTypedValue(string key)
 373    {
 374        return null;
 375    }
 376    /// <summary>
 377    /// Gets whether or not the settings set is mutable.
 378    /// </summary>
 379    public bool SettingsAreMutable => false;
 380    /// <summary>
 381    /// Changes the specified setting, if possible.
 382    /// For many ambient settings services, the value will only be reflected in memory until the process shuts down, but
 383    /// </summary>
 384    /// <param name="key">A string that uniquely identifies the setting.</param>
 385    /// <param name="value">The new string value for the setting, or null if the setting should be removed.</param>
 386    /// <returns>Whether or not the setting actually changed (it may have had already the same value).</returns>
 387    public bool ChangeSetting(string key, string? value) => throw new InvalidOperationException($"{nameof(DefaultSetting
 388}
 389
 390/// <summary>
 391/// The realization of <see cref="IAmbientSettingInfo"/> that backs every setting declared through <see cref="AmbientSet
 392/// </summary>
 393/// <remarks>
 394/// <pitch>The single realization behind every declared setting: it owns the conversion delegate, the typed default, the
 395/// <pledge><see cref="IAmbientSettingInfo"/></pledge>
 396/// <pledge>Registers itself in <see cref="SettingsRegistry.DefaultRegistry"/> at construction, so constructing one whos
 397/// <plan>Conversion wraps the caller's delegate in a catch-all that reports failures through <see cref="AmbientSettings
 398/// </remarks>
 399/// <typeparam name="T">The type for the setting.</typeparam>
 400internal class SettingInfo<T> : IAmbientSettingInfo
 401{
 402    protected static readonly AmbientService<IAmbientSettingsSet> _SettingsSet = AmbientService<IAmbientSettingsSet>.Ins
 403    private readonly Func<string, T> _convert;
 404    private long _lastUsedTicks = DateTime.MinValue.Ticks;       // interlocked
 405    private SettingsSetSettingValue<T>? _globalSetAndValue;      // interlocked
 406
 407    public SettingInfo(string key, string description, T defaultValue, Func<string, T>? convert)
 408    {
 409        if (convert == null)
 410        {
 411            if (typeof(T) != typeof(string)) throw new ArgumentNullException(nameof(convert));
 412            convert = s => ((T)(object)s)!; // this should be okay because we've just tested the type above and we only 
 413        }
 414        Key = key;
 415        Description = description;
 416        _convert = convert;
 417        DefaultValue = defaultValue;
 418        DefaultValueString = defaultValue?.ToString() ?? "";
 419        SettingsRegistry.DefaultRegistry.Register(this);
 420    }
 421
 422    public SettingInfo(string key, string description, Func<string, T>? convert, string defaultValue = "")
 423    {
 424        if (convert == null)
 425        {
 426            if (typeof(T) != typeof(string)) throw new ArgumentNullException(nameof(convert));
 427            convert = s => (T)(object)s;
 428        }
 429        Key = key;
 430        Description = description;
 431        _convert = convert;
 432        DefaultValueString = defaultValue;
 433        DefaultValue = convert(defaultValue);
 434        SettingsRegistry.DefaultRegistry.Register(this);
 435    }
 436
 437    /// <summary>
 438    /// Gets the setting key.
 439    /// </summary>
 440    public string Key { get; }
 441    /// <summary>
 442    /// Gets a description of the setting.
 443    /// </summary>
 444    public string Description { get; }
 445    /// <summary>
 446    /// Gets the last time the setting's value was retrieved.
 447    /// </summary>
 448    public DateTime LastUsed => new(_lastUsedTicks);
 449    /// <summary>
 450    /// Gets the default value for the setting.
 451    /// </summary>
 452    public string DefaultValueString { get; }
 453    /// <summary>
 454    /// Gets the typed default value.
 455    /// </summary>
 456    public T DefaultValue { get; }
 457    /// <summary>
 458    /// Gets the untyped default value.
 459    /// </summary>
 460    object? IAmbientSettingInfo.DefaultValue => DefaultValue;
 461
 462    /// <summary>
 463    /// Converts the specified value for the specified settings set.
 464    /// </summary>
 465    /// <param name="settingsSet">The <see cref="IAmbientSettingsSet"/> for the implementation doing the conversion.</pa
 466    /// <param name="value">The string value for the setting.</param>
 467    /// <returns>A typed value created from the string value.</returns>
 468    public object Convert(IAmbientSettingsSet settingsSet, string value)
 469    {
 470        T ret;
 471        try
 472        {
 473            ret = _convert(value);
 474        }
 475#pragma warning disable CA1031 // this is a "do your best" kind of function, so we really do want to catch all exception
 476        catch (Exception ex)
 477#pragma warning restore CA1031
 478        {
 479            AmbientSettings.NotifyConversionFailed(Key, value, ex, DefaultValue);
 480            ret = DefaultValue;
 481        }
 482        // is this settings set the one for this setting or is it the global settings set?
 483        if (settingsSet == _SettingsSet.Global)
 484        {
 485            System.Threading.Interlocked.Exchange(ref _globalSetAndValue, new SettingsSetSettingValue<T>(ret, settingsSe
 486        }
 487        return ret!;
 488    }
 489    /// <summary>
 490    /// Updates the last used time for this setting to the current time.
 491    /// </summary>
 492    public void UpdateLastUsed()
 493    {
 494        long accessTime = AmbientClock.UtcNow.Ticks;
 495        long oldValue = _lastUsedTicks;
 496        // loop attempting to put it in until we win the race
 497        while (accessTime > oldValue)
 498        {
 499            // Coverage note: this loop is nondeterministic when running with multiple threads, so code coverage may not
 500            // try to put in our value--did we win the race?
 501            if (oldValue == System.Threading.Interlocked.CompareExchange(ref _lastUsedTicks, accessTime, oldValue))
 502            {
 503                // we're done and we were the new max
 504                break;
 505            }
 506            // update our value
 507            oldValue = _lastUsedTicks;
 508        }
 509        // if we didn't break, we're done but the existing value is the max, not this one
 510    }
 511    /// <summary>
 512    /// Gets the current value of the setting (cached from the value given by the settings set).
 513    /// </summary>
 514    public T GlobalOrDefaultValue
 515    {
 516        get
 517        {
 518            UpdateLastUsed();
 519            return (_globalSetAndValue != null) ? _globalSetAndValue.Value : DefaultValue;
 520        }
 521    }
 522    /// <summary>
 523    /// Gets the current value of the setting and the settings set it came from (cached from the value given by the sett
 524    /// </summary>
 525    public SettingsSetSettingValue<T> GlobalSetAndValue
 526    {
 527        get
 528        {
 529            UpdateLastUsed();
 530            return _globalSetAndValue ?? new SettingsSetSettingValue<T>(DefaultValue, DefaultSettingsSet.Instance);
 531        }
 532    }
 533}
 534
 535/// <summary>
 536/// A realization of <see cref="IAmbientSetting{T}"/> whose value comes from a specific settings set.
 537/// </summary>
 538/// <remarks>
 539/// <pitch>The realization of <see cref="IAmbientSetting{T}"/> for settings pinned to one specific settings set at decla
 540/// <pledge><see cref="IAmbientSetting{T}"/></pledge>
 541/// <plan>Holds a <see cref="SettingInfo{T}"/> (created — and therefore registered — at construction) plus the pinned se
 542/// </remarks>
 543/// <typeparam name="T">The type for the setting.</typeparam>
 544internal class SettingsSetSetting<T> : IAmbientSetting<T>
 545{
 546    protected static readonly AmbientService<IAmbientSettingsSet> _AmbientSettingsSet = AmbientService<IAmbientSettingsS
 547
 548    protected readonly AmbientService<IAmbientSettingsSet>? _settingsSet;
 549    protected readonly SettingInfo<T> _settingInfo;
 550    private readonly IAmbientSettingsSet? _fixedSettingsSet;
 551
 552    public SettingsSetSetting(IAmbientSettingsSet? fixedSettingsSet, string key, string description, Func<string, T>? co
 553    {
 554        _settingInfo = new SettingInfo<T>(key, description, convert, defaultValueString);
 555        _fixedSettingsSet = fixedSettingsSet;
 556    }
 557
 558    public SettingsSetSetting(IAmbientSettingsSet? fixedSettingsSet, string key, string description, T defaultValue, Fun
 559    {
 560        _settingInfo = new SettingInfo<T>(key, description, defaultValue, convert);
 561        _fixedSettingsSet = fixedSettingsSet;
 562    }
 563
 564    internal SettingsSetSetting(AmbientService<IAmbientSettingsSet> settings, string key, string description, Func<strin
 565    {
 566        _settingInfo = new SettingInfo<T>(key, description, convert, defaultValueString);
 567        _settingsSet = settings;
 568    }
 569#if NEEDED
 570    internal SettingsSetSetting(AmbientService<IAmbientSettingsSet> settings, string key, string description, T defaultV
 571    {
 572        _settingInfo = new SettingInfo<T>(key, description, defaultValue, convert);
 573        _settingsSet = settings;
 574    }
 575#endif
 576    protected IAmbientSettingsSet? GetValueSet()
 577    {
 578        _settingInfo.UpdateLastUsed();
 579        // internal override for testing?
 580        if (_settingsSet != null)
 581        {
 582            return _settingsSet.Local;
 583        }
 584        // is there a fixed settings set?
 585        else if (_fixedSettingsSet != null)
 586        {
 587            return _fixedSettingsSet;
 588        }
 589        // otherwise use the current ambient settings set
 590        return _AmbientSettingsSet.Local;
 591    }
 592    protected T GetValueFromSet(IAmbientSettingsSet set)
 593    {
 594        object? value = set.GetTypedValue(_settingInfo.Key);
 595        return (value == null) ? _settingInfo.DefaultValue : (T)value;
 596    }
 597    protected (T, string) GetValueAndSet(IAmbientSettingsSet set)
 598    {
 599        object? value = set.GetTypedValue(_settingInfo.Key);
 600        return (value == null) ? (_settingInfo.DefaultValue, DefaultSettingsSet.Instance.SetName) : ((T)value, set.SetNa
 601    }
 602    /// <summary>
 603    /// Gets the key of the setting, which may be useful in providing an override without having to match the name with 
 604    /// </summary>
 605    public string Key => _settingInfo.Key;
 606    /// <summary>
 607    /// Gets the current value of the setting (cached from the value given by the settings set).
 608    /// </summary>
 609    public virtual T Value
 610    {
 611        get
 612        {
 613            IAmbientSettingsSet? set = GetValueSet();
 614            return (set != null)
 615                ? GetValueFromSet(set)
 616                : _settingInfo.GlobalOrDefaultValue;
 617        }
 618    }
 619    /// <summary>
 620    /// Gets the current value of the setting along with the name of the set that the value came from.
 621    /// Note that this function may be significantly slower than <see cref="Value"/>.
 622    /// </summary>
 623    /// <returns>The current value of the setting along with the name of the set that the value came from.</returns>
 624    public virtual (T, string) GetValueWithSetName()
 625    {
 626        IAmbientSettingsSet? set = GetValueSet();
 627        return (set != null)
 628            ? GetValueAndSet(set)
 629            : (_settingInfo.GlobalOrDefaultValue, DefaultSettingsSet.Instance.SetName);
 630    }
 631    /// <summary>
 632    /// Gets a string representation of the setting, which may be useful for debugging.  Note that this is not guarantee
 633    /// </summary>
 634    /// <returns>A string representation of the setting.</returns>
 635    public override string ToString()
 636    {
 637        (T value, string setName) = GetValueWithSetName();
 638        return $"{setName}:{value}";
 639    }
 640}
 641
 642/// <summary>
 643/// A realization of <see cref="IAmbientSetting{T}"/> whose value follows the ambient settings set.
 644/// </summary>
 645/// <remarks>
 646/// <pitch>The realization of <see cref="IAmbientSetting{T}"/> that follows the ambient settings service — honoring a ca
 647/// <pledge><see cref="IAmbientSetting{T}"/></pledge>
 648/// <plan>Extends <see cref="SettingsSetSetting{T}"/> by resolving the set through the ambient <see cref="AmbientService
 649/// </remarks>
 650/// <typeparam name="T">The type for the setting.</typeparam>
 651internal class AmbientSetting<T> : SettingsSetSetting<T>
 652{
 653    public AmbientSetting(string key, string description, Func<string, T> convert, string defaultValueString = "")
 654        : base((IAmbientSettingsSet?)null, key, description, convert, defaultValueString)
 655    {
 656    }
 657
 658    public AmbientSetting(string key, string description, T defaultValue, Func<string, T> convert)
 659        : base(null, key, description, defaultValue, convert)
 660    {
 661    }
 662
 663    internal AmbientSetting(AmbientService<IAmbientSettingsSet> settingsSet, string key, string description, Func<string
 664        : base(settingsSet, key, description, convert, defaultValueString)
 665    {
 666    }
 667#if NEEDED // Add this one if needed--it should work fine, but isn't currently used
 668    internal AmbientSetting(AmbientService<IAmbientSettingsSet> settingsSet, string key, string description, T defaultVa
 669        : base(settingsSet, key, description, defaultValue, convert)
 670    {
 671    }
 672#endif
 673    private IAmbientSettingsSet? GetAmbientValueSet()
 674    {
 675        _settingInfo.UpdateLastUsed();
 676        AmbientService<IAmbientSettingsSet> settingsSet = _settingsSet ?? _AmbientSettingsSet;
 677        // is there a local settings set override?
 678        IAmbientSettingsSet? localSettingsSetOverride = settingsSet.Override;
 679        if (localSettingsSetOverride != null) return localSettingsSetOverride;
 680        // is there a local settings set suppression?
 681        IAmbientSettingsSet? localSettingsSet = settingsSet.Local;
 682        return (localSettingsSet != null)
 683            ? GetValueSet() // fall through to the base (global settings set)
 684            : null;  // service suppressed: no set is consulted, so the value falls back to the cached global-set value 
 685    }
 686
 687    /// <summary>
 688    /// Gets the current value of the setting (cached from the value given by the settings set).
 689    /// </summary>
 690    public override T Value
 691    {
 692        get
 693        {
 694            IAmbientSettingsSet? set = GetAmbientValueSet();
 695            return (set != null)
 696                ? GetValueFromSet(set)
 697                : _settingInfo.GlobalOrDefaultValue;
 698        }
 699    }
 700    /// <summary>
 701    /// Gets the current value of the setting along with the name of the set that the value came from (or null if the de
 702    /// Note that this function may be significantly slower than <see cref="Value"/>.
 703    /// </summary>
 704    /// <returns>The current value of the setting along with the name of the set that the value came from (or null if th
 705    public override (T, string) GetValueWithSetName()
 706    {
 707        IAmbientSettingsSet? set = GetAmbientValueSet();
 708        if (set != null) return GetValueAndSet(set);
 709        SettingsSetSettingValue<T> setAndValue = _settingInfo.GlobalSetAndValue;
 710        return (setAndValue.Value, setAndValue.SettingsSet.SetName);
 711    }
 712}