diff --git a/src/libraries/Microsoft.Extensions.Configuration/ref/Microsoft.Extensions.Configuration.cs b/src/libraries/Microsoft.Extensions.Configuration/ref/Microsoft.Extensions.Configuration.cs index 31de97acabf21a..ffcbf3c2995e81 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/ref/Microsoft.Extensions.Configuration.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/ref/Microsoft.Extensions.Configuration.cs @@ -97,16 +97,28 @@ public ConfigurationSection(Microsoft.Extensions.Configuration.IConfigurationRoo public string Key { get { throw null; } } public string Path { get { throw null; } } public string? Value { get { throw null; } set { } } - public bool TryGetValue(string? key, out string? value) { throw null; } public System.Collections.Generic.IEnumerable GetChildren() { throw null; } public Microsoft.Extensions.Primitives.IChangeToken GetReloadToken() { throw null; } public Microsoft.Extensions.Configuration.IConfigurationSection GetSection(string key) { throw null; } + public bool TryGetValue(string? key, out string? value) { throw null; } } public static partial class MemoryConfigurationBuilderExtensions { public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddInMemoryCollection(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder) { throw null; } public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddInMemoryCollection(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, System.Collections.Generic.IEnumerable>? initialData) { throw null; } } + public enum ReferenceMode + { + Ignore = 0, + Read = 1, + Scan = 2, + } + public static partial class ReferenceResolutionConfigurationBuilderExtensions + { + public static Microsoft.Extensions.Configuration.IConfigurationBuilder SetReferenceMode(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, Microsoft.Extensions.Configuration.IConfigurationSource source, Microsoft.Extensions.Configuration.ReferenceMode mode) { throw null; } + public static Microsoft.Extensions.Configuration.IConfigurationBuilder SetReferenceMode(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, Microsoft.Extensions.Configuration.ReferenceMode mode) { throw null; } + public static Microsoft.Extensions.Configuration.IConfigurationBuilder SetReferenceMode(this Microsoft.Extensions.Configuration.IConfigurationBuilder configurationBuilder, System.Collections.Generic.IEnumerable sources, Microsoft.Extensions.Configuration.ReferenceMode mode) { throw null; } + } public abstract partial class StreamConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider { public StreamConfigurationProvider(Microsoft.Extensions.Configuration.StreamConfigurationSource source) { } diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationBuilder.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationBuilder.cs index 75b930f60f16eb..18a5b4fa9a8b00 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationBuilder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationBuilder.cs @@ -47,10 +47,17 @@ public IConfigurationRoot Build() var providers = new List(); foreach (IConfigurationSource source in _sources) { - IConfigurationProvider provider = source.Build(this); - providers.Add(provider); + providers.Add(source.Build(this)); } - return new ConfigurationRoot(providers); + + ReferenceResolutionEngine? engine = null; + if (ReferenceResolutionConfigurationBuilderExtensions.HasAnyScanSource(Properties, _sources)) + { + Dictionary? providerModes = ReferenceResolutionConfigurationBuilderExtensions + .ResolveProviderModes(Properties, _sources, providers); + engine = new ReferenceResolutionEngine(providers, providerModes); + } + return new ConfigurationRoot(providers, engine); } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs index b31c4bb0c6392e..21ddcda1dd8de0 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs @@ -37,6 +37,12 @@ public sealed class ConfigurationManager : IConfigurationManager, IConfiguration private readonly List _changeTokenRegistrations = new(); private ConfigurationReloadToken _changeToken = new(); + // Non-null when the builder opted into reference resolution via EnableReferenceResolution. + // Rebuilt on every source mutation (AddSource/ReloadSources) so it always reflects the + // current provider set. Reads are unsynchronized; in-flight reads that observe a stale + // engine still see a consistent (old) provider snapshot held by that engine. + private ReferenceResolutionEngine? _engine; + /// /// Creates an empty mutable configuration object that is both an and an . /// @@ -55,6 +61,12 @@ public string? this[string key] get { using ReferenceCountedProviders reference = _providerManager.GetReference(); + ReferenceResolutionEngine? engine = _engine; + if (engine is not null) + { + return engine.TryGet(key, out string? resolved) ? resolved : null; + } + return ConfigurationRoot.GetConfiguration(reference.Providers, key); } set @@ -64,6 +76,8 @@ public string? this[string key] } } + internal ReferenceResolutionEngine? Engine => _engine; + /// public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key); @@ -84,6 +98,7 @@ public string? this[string key] public void Dispose() { DisposeRegistrations(); + Interlocked.Exchange(ref _engine, null)?.Dispose(); _providerManager.Dispose(); } @@ -109,6 +124,7 @@ void IConfigurationRoot.Reload() } } + _engine?.Invalidate(); RaiseChanged(); } @@ -129,6 +145,7 @@ private void AddSource(IConfigurationSource source) _changeTokenRegistrations.Add(ChangeToken.OnChange(provider.GetReloadToken, RaiseChanged)); _providerManager.AddProvider(provider); + SwapEngine(); RaiseChanged(); } @@ -153,9 +170,30 @@ private void ReloadSources() } _providerManager.ReplaceProviders(newProvidersList); + SwapEngine(); RaiseChanged(); } + // Rebuild the engine against the current provider set when resolution is enabled. + // The old engine's providers are the previous snapshot, and any in-flight read that + // already captured the old engine will complete against that snapshot; a subsequent + // read will pick up the new engine. The old engine is disposed to drop its reload- + // token subscription against the old providers. + private void SwapEngine() + { + ReferenceResolutionEngine? newEngine = null; + if (ReferenceResolutionConfigurationBuilderExtensions.HasAnyScanSource(_properties.Raw, _sources)) + { + IReadOnlyList providers = _providerManager.GetProvidersSnapshot(); + Dictionary? providerModes = ReferenceResolutionConfigurationBuilderExtensions + .ResolveProviderModes(_properties.Raw, _sources, providers); + newEngine = new ReferenceResolutionEngine(providers, providerModes); + } + + ReferenceResolutionEngine? previous = Interlocked.Exchange(ref _engine, newEngine); + previous?.Dispose(); + } + private void DisposeRegistrations() { // dispose change token registrations @@ -276,7 +314,14 @@ public object this[string key] set { _properties[key] = value; - _config.ReloadSources(); + if (IsReferenceResolutionProperty(key)) + { + _config.SwapEngine(); + } + else + { + _config.ReloadSources(); + } } } @@ -291,13 +336,27 @@ public object this[string key] public void Add(string key, object value) { _properties.Add(key, value); - _config.ReloadSources(); + if (IsReferenceResolutionProperty(key)) + { + _config.SwapEngine(); + } + else + { + _config.ReloadSources(); + } } public void Add(KeyValuePair item) { ((IDictionary)_properties).Add(item); - _config.ReloadSources(); + if (IsReferenceResolutionProperty(item.Key)) + { + _config.SwapEngine(); + } + else + { + _config.ReloadSources(); + } } public void Clear() @@ -329,17 +388,43 @@ public IEnumerator> GetEnumerator() public bool Remove(string key) { var wasRemoved = _properties.Remove(key); - _config.ReloadSources(); + if (IsReferenceResolutionProperty(key)) + { + _config.SwapEngine(); + } + else + { + _config.ReloadSources(); + } + return wasRemoved; } public bool Remove(KeyValuePair item) { var wasRemoved = ((IDictionary)_properties).Remove(item); - _config.ReloadSources(); + if (IsReferenceResolutionProperty(item.Key)) + { + _config.SwapEngine(); + } + else + { + _config.ReloadSources(); + } + return wasRemoved; } + // Reference-resolution property writes only affect how the root interprets the existing + // provider set; they never change which providers exist or what they hold. Handling them + // through SwapEngine avoids the O(n) provider rebuild/Load cost that a general property + // write triggers via ReloadSources, so enabling or reconfiguring resolution mid-setup + // does not penalize callers using slow-to-load sources (e.g. Azure App Configuration). + private static bool IsReferenceResolutionProperty(string key) + { + return key == ReferenceResolutionConfigurationBuilderExtensions.SourceModesPropertyName; + } + public bool TryGetValue(string key, [NotNullWhen(true)] out object? value) { return _properties.TryGetValue(key, out value); @@ -349,6 +434,11 @@ IEnumerator IEnumerable.GetEnumerator() { return _properties.GetEnumerator(); } + + // Direct accessor used by the ConfigurationManager build/reload loop to mutate + // well-known properties (e.g. PreviouslyBuiltProviders) without triggering another + // ReloadSources via the IDictionary interface this class exposes publicly. + internal IDictionary Raw => _properties; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs index bd53b747a43822..d260a11c643965 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs @@ -17,6 +17,7 @@ namespace Microsoft.Extensions.Configuration public class ConfigurationRoot : IConfigurationRoot, IDisposable { private readonly IList _providers; + private readonly ReferenceResolutionEngine? _engine; private readonly List _changeTokenRegistrations; private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken(); @@ -25,10 +26,16 @@ public class ConfigurationRoot : IConfigurationRoot, IDisposable /// /// The s for this configuration. public ConfigurationRoot(IList providers) + : this(providers, engine: null) + { + } + + internal ConfigurationRoot(IList providers, ReferenceResolutionEngine? engine) { ArgumentNullException.ThrowIfNull(providers); _providers = providers; + _engine = engine; _changeTokenRegistrations = new List(providers.Count); foreach (IConfigurationProvider p in providers) { @@ -42,6 +49,8 @@ public ConfigurationRoot(IList providers) /// public IEnumerable Providers => _providers; + internal ReferenceResolutionEngine? Engine => _engine; + /// /// Gets or sets the value corresponding to a configuration key. /// @@ -49,7 +58,15 @@ public ConfigurationRoot(IList providers) /// The configuration value. public string? this[string key] { - get => GetConfiguration(_providers, key); + get + { + if (_engine is not null) + { + return _engine.TryGet(key, out string? resolved) ? resolved : null; + } + + return GetConfiguration(_providers, key); + } set => SetConfiguration(_providers, key, value); } @@ -86,6 +103,7 @@ public void Reload() { provider.Load(); } + _engine?.Invalidate(); RaiseChanged(); } @@ -109,6 +127,8 @@ public void Dispose() { (provider as IDisposable)?.Dispose(); } + + _engine?.Dispose(); } internal static string? GetConfiguration(IList providers, string key) diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs b/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs index a4a57d45d59930..0666b0b8947409 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs @@ -23,9 +23,17 @@ internal static IEnumerable GetChildrenImplementation(thi using ReferenceCountedProviders? reference = (root as ConfigurationManager)?.GetProvidersReference(); IEnumerable providers = reference?.Providers ?? root.Providers; - IEnumerable children = providers + IEnumerable keys = providers .Aggregate(Enumerable.Empty(), - (seed, source) => source.GetChildKeys(seed, path)) + (seed, source) => source.GetChildKeys(seed, path)); + + ReferenceResolutionEngine? engine = (root as ConfigurationRoot)?.Engine ?? (root as ConfigurationManager)?.Engine; + if (engine is not null) + { + keys = engine.GetChildKeys(keys, path); + } + + IEnumerable children = keys .Distinct(StringComparer.OrdinalIgnoreCase) .Select(key => root.GetSection(path == null ? key : path + ConfigurationPath.KeyDelimiter + key)); @@ -42,6 +50,18 @@ internal static IEnumerable GetChildrenImplementation(thi internal static bool TryGetConfiguration(this IConfigurationRoot root, string key, out string? value) { + ReferenceResolutionEngine? engine = (root as ConfigurationRoot)?.Engine ?? (root as ConfigurationManager)?.Engine; + if (engine is not null) + { + if (engine.TryGet(key, out value)) + { + return true; + } + + value = null; + return false; + } + // common cases Providers is IList in ConfigurationRoot IList providers = root.Providers is IList list ? list diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProvidersManager.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProvidersManager.cs index 210e41f665a47a..70f3fa8ed2702e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProvidersManager.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProvidersManager.cs @@ -75,6 +75,22 @@ public void AddProvider(IConfigurationProvider provider) } } + // Returns an immutable snapshot of the currently-built providers. Used by the builder's + // source.Build(...) loop so a ReferenceResolutionConfigurationSource can observe providers + // registered before it. + public IReadOnlyList GetProvidersSnapshot() + { + lock (_replaceProvidersLock) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(ConfigurationManager)); + } + + return _refCountedProviders.Providers.ToArray(); + } + } + public void Dispose() { ReferenceCountedProviders oldRefCountedProviders = _refCountedProviders; diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceMode.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceMode.cs new file mode 100644 index 00000000000000..514ce194ae7950 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceMode.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration +{ + /// + /// Controls how an participates in {{…}} + /// reference resolution performed by the built from the + /// containing . + /// + /// + /// Values are totally ordered by participation: < + /// < . Sources default to unless configured via + /// + /// or one of its overloads. The reference-resolution engine is attached to the built root + /// only when at least one source has been explicitly marked . + /// + public enum ReferenceMode + { + /// + /// The source is invisible to the reference-resolution engine: no {{…}} + /// reference or section alias in another source can reach its values. Direct reads + /// via the normal API still return its values. + /// + Ignore = 0, + + /// + /// The source is a valid substitution target for references in other + /// sources. The source's own values are returned verbatim — + /// ref(...) / format(...) sequences are not interpreted. This is + /// the default mode for sources that have not been configured explicitly. + /// + Read = 1, + + /// + /// The source's values are scanned for ref(...) / format(...) tokens + /// and section aliases, and the source is exposed as a substitution target for + /// other sources. Marking at least one source with this value + /// activates the reference-resolution engine. + /// + Scan = 2, + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceParser.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceParser.cs new file mode 100644 index 00000000000000..3dee543019b4f9 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceParser.cs @@ -0,0 +1,781 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Extensions.Configuration +{ + // Value syntax: + // ref() — reference; may resolve to a section. + // format(