diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationBuilder.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationBuilder.cs
index 75b930f60f16eb..ff6fc68dcf063d 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationBuilder.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationBuilder.cs
@@ -44,11 +44,10 @@ public IConfigurationBuilder Add(IConfigurationSource source)
/// An with keys and values from the registered providers.
public IConfigurationRoot Build()
{
- var providers = new List();
+ var providers = new List(_sources.Count);
foreach (IConfigurationSource source in _sources)
{
- IConfigurationProvider provider = source.Build(this);
- providers.Add(provider);
+ providers.Add(source.Build(this));
}
return new ConfigurationRoot(providers);
}
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs
index b31c4bb0c6392e..9902eec6047942 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs
@@ -55,12 +55,22 @@ public string? this[string key]
get
{
using ReferenceCountedProviders reference = _providerManager.GetReference();
- return ConfigurationRoot.GetConfiguration(reference.Providers, key);
+ if (ReferenceEngine.Disabled)
+ {
+ return ConfigurationRoot.GetConfiguration(reference.Providers, key);
+ }
+
+ // Read the engine once and resolve against its own providers. AddProvider swaps this generation's
+ // provider list and engine in place, so reading the two separately could pair an old list with a newer
+ // engine (or the reverse) and resolve the key against the wrong providers.
+ reference.ReferenceEngine!.TryRead(key, out string? value);
+ return value;
}
set
{
using ReferenceCountedProviders reference = _providerManager.GetReference();
ConfigurationRoot.SetConfiguration(reference.Providers, key, value);
+ _providerManager.ResetReferenceEngine();
}
}
@@ -116,6 +126,7 @@ void IConfigurationRoot.Reload()
private void RaiseChanged()
{
+ _providerManager.ResetReferenceEngine();
var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationProvider.cs
index 3f39758d25b797..b14d3670635669 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationProvider.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationProvider.cs
@@ -29,6 +29,10 @@ protected ConfigurationProvider()
///
protected IDictionary Data { get; set; }
+ // Exposes this provider's key/value pairs so the reference engine can build its reference index by scanning the
+ // already-loaded backing dictionary directly, instead of going through the O(n^2) GetChildKeys reconstruction.
+ internal IEnumerable> GetEntriesForReferenceScan() => Data;
+
///
/// Attempts to find a value with the given key.
///
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs
index bd53b747a43822..22b4b279120c3d 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs
@@ -20,6 +20,11 @@ public class ConfigurationRoot : IConfigurationRoot, IDisposable
private readonly List _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
+ // Per-root reference state, bound to the (fixed) provider list and swapped for a fresh instance on every reload
+ // (see RaiseChanged) so a reload can never surface stale reference information. When no provider declares any
+ // $ref the index is empty, so reads take the plain provider path with no reference overhead.
+ private volatile ReferenceEngine? _referenceEngine;
+
///
/// Initializes a Configuration root with a list of providers.
///
@@ -29,6 +34,7 @@ public ConfigurationRoot(IList providers)
ArgumentNullException.ThrowIfNull(providers);
_providers = providers;
+ _referenceEngine = ReferenceEngine.Create(providers);
_changeTokenRegistrations = new List(providers.Count);
foreach (IConfigurationProvider p in providers)
{
@@ -49,8 +55,21 @@ public ConfigurationRoot(IList providers)
/// The configuration value.
public string? this[string key]
{
- get => GetConfiguration(_providers, key);
- set => SetConfiguration(_providers, key, value);
+ get
+ {
+ if (ReferenceEngine.Disabled)
+ {
+ return GetConfiguration(_providers, key);
+ }
+
+ _referenceEngine!.TryRead(key, out string? value);
+ return value;
+ }
+ set
+ {
+ SetConfiguration(_providers, key, value);
+ _referenceEngine = ReferenceEngine.Create(_providers);
+ }
}
///
@@ -91,10 +110,13 @@ public void Reload()
private void RaiseChanged()
{
+ _referenceEngine = ReferenceEngine.Create(_providers);
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
+ internal ReferenceEngine? ReferenceEngine => _referenceEngine;
+
///
public void Dispose()
{
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs b/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs
index a4a57d45d59930..5bfc5a71b65058 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs
@@ -20,13 +20,31 @@ internal static class InternalConfigurationRootExtensions
/// Immediate children sub-sections of section specified by key.
internal static IEnumerable GetChildrenImplementation(this IConfigurationRoot root, string? path)
{
+ // For ConfigurationManager the engine is obtained from the pinned provider generation, so the engine and the
+ // providers it resolves against are always the same generation. A plain ConfigurationRoot exposes its engine
+ // directly (its provider list is fixed).
using ReferenceCountedProviders? reference = (root as ConfigurationManager)?.GetProvidersReference();
- IEnumerable providers = reference?.Providers ?? root.Providers;
+ ReferenceEngine? engine = reference?.ReferenceEngine ?? (root as ConfigurationRoot)?.ReferenceEngine;
- IEnumerable children = providers
- .Aggregate(Enumerable.Empty(),
- (seed, source) => source.GetChildKeys(seed, path))
- .Distinct(StringComparer.OrdinalIgnoreCase)
+ IEnumerable childKeys;
+ if (ReferenceEngine.Disabled || engine is null || engine.IndexIsEmpty)
+ {
+ // No engine (references disabled, or a third-party root), or this generation declares no references:
+ // enumerate children the plain way, with nothing to resolve, merge or hide, and allocate exactly like a
+ // reference-free configuration. Use the engine's provider list when present so it matches the generation.
+ IEnumerable providers = engine?.Providers ?? reference?.Providers ?? root.Providers;
+ childKeys = providers
+ .Aggregate(Enumerable.Empty(), (seed, source) => source.GetChildKeys(seed, path))
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+ }
+ else
+ {
+ // References merge into the children: a redirected section lists its target's children unioned with any
+ // keys a higher provider defines under the reference, at every hop of the chain.
+ childKeys = engine.ChildKeys(path);
+ }
+
+ IEnumerable children = childKeys
.Select(key => root.GetSection(path == null ? key : path + ConfigurationPath.KeyDelimiter + key));
if (reference is null)
@@ -42,19 +60,37 @@ internal static IEnumerable GetChildrenImplementation(thi
internal static bool TryGetConfiguration(this IConfigurationRoot root, string key, out string? value)
{
- // common cases Providers is IList in ConfigurationRoot
+ // For ConfigurationManager, take a counted reference and use the engine from that pinned generation
+ // so a concurrent source mutation can neither dispose the providers mid-read nor pair them with a different
+ // generation's index. The resolved key is then read like any other.
+ if (!ReferenceEngine.Disabled)
+ {
+ if (root is ConfigurationManager cm)
+ {
+ using ReferenceCountedProviders reference = cm.GetProvidersReference();
+ return reference.ReferenceEngine!.TryRead(key, out value);
+ }
+ else if (root is ConfigurationRoot cr)
+ {
+ return cr.ReferenceEngine!.TryRead(key, out value);
+ }
+ }
+
+ // Plain path: references globally disabled, or a third-party root implementation.
+ // Commonly Providers is already IList in ConfigurationRoot.
IList providers = root.Providers is IList list
? list
: root.Providers.ToList();
+ return TryGet(providers, key, out value);
+ }
- // ensure looping in the reverse order
+ private static bool TryGet(IList providers, string key, out string? value)
+ {
for (int i = providers.Count - 1; i >= 0; i--)
{
- IConfigurationProvider provider = providers[i];
-
try
{
- if (provider.TryGet(key, out value))
+ if (providers[i].TryGet(key, out value))
{
return true;
}
@@ -76,6 +112,5 @@ internal static bool TryGetConfiguration(this IConfigurationRoot root, string ke
value = null;
return false;
}
-
}
}
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/Microsoft.Extensions.Configuration.csproj b/src/libraries/Microsoft.Extensions.Configuration/src/Microsoft.Extensions.Configuration.csproj
index cb2fe733e9cb41..58461d782758fd 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/src/Microsoft.Extensions.Configuration.csproj
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/Microsoft.Extensions.Configuration.csproj
@@ -14,6 +14,11 @@
Link="Common\src\Extensions\EmptyDisposable.cs" />
+
+
+
+
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProviders.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProviders.cs
index 2c2e32fc7e47d0..b94863d0cdd7e1 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProviders.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProviders.cs
@@ -29,16 +29,22 @@ internal abstract class ReferenceCountedProviders : IDisposable
// This is Dispose() rather than RemoveReference() so we can conveniently release a reference at the end of a using block.
public abstract void Dispose();
+ public abstract ReferenceEngine? ReferenceEngine { get; }
+
+ public abstract void ResetReferenceEngine();
+
private sealed class ActiveReferenceCountedProviders : ReferenceCountedProviders
{
private long _refCount = 1;
// volatile is not strictly necessary because the runtime adds a barrier either way, but volatile indicates that this field has
// unsynchronized readers meaning the all writes initializing the list must be published before updating the _providers reference.
private volatile List _providers;
+ private volatile ReferenceEngine? _referenceEngine;
public ActiveReferenceCountedProviders(List providers)
{
_providers = providers;
+ _referenceEngine = ReferenceEngine.Create(providers);
}
public override List Providers
@@ -52,11 +58,16 @@ public override List Providers
{
Debug.Assert(_refCount > 0);
_providers = value;
+ _referenceEngine = ReferenceEngine.Create(value);
}
}
public override List NonReferenceCountedProviders => _providers;
+ public override ReferenceEngine? ReferenceEngine => _referenceEngine;
+
+ public override void ResetReferenceEngine() => _referenceEngine = ReferenceEngine.Create(_providers);
+
public override void AddReference()
{
// AddReference() is always called with a lock to ensure _refCount hasn't already decremented to zero.
@@ -78,14 +89,21 @@ public override void Dispose()
private sealed class DisposedReferenceCountedProviders : ReferenceCountedProviders
{
+ private volatile ReferenceEngine? _referenceEngine;
+
public DisposedReferenceCountedProviders(List providers)
{
Providers = providers;
+ _referenceEngine = ReferenceEngine.Create(providers);
}
public override List Providers { get; set; }
public override List NonReferenceCountedProviders => Providers;
+ public override ReferenceEngine? ReferenceEngine => _referenceEngine;
+
+ public override void ResetReferenceEngine() => _referenceEngine = ReferenceEngine.Create(Providers);
+
public override void AddReference() { }
public override void Dispose() { }
}
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProvidersManager.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProvidersManager.cs
index 210e41f665a47a..6faf9732c71c34 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProvidersManager.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceCountedProvidersManager.cs
@@ -75,6 +75,17 @@ public void AddProvider(IConfigurationProvider provider)
}
}
+ public void ResetReferenceEngine()
+ {
+ lock (_replaceProvidersLock)
+ {
+ if (!_disposed)
+ {
+ _refCountedProviders.ResetReferenceEngine();
+ }
+ }
+ }
+
public void Dispose()
{
ReferenceCountedProviders oldRefCountedProviders = _refCountedProviders;
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceEngine.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceEngine.cs
new file mode 100644
index 00000000000000..110b121b5b991b
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceEngine.cs
@@ -0,0 +1,214 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Microsoft.Extensions.Configuration
+{
+ // The reference engine for one provider generation: it lazily builds and memoizes a ReferenceIndex from the
+ // generation's providers, then resolves keys against it. Bound to the exact provider list it serves and swapped for
+ // a fresh instance whenever that list or the providers' data change (a new ReferenceCountedProviders generation, or
+ // a ConfigurationRoot reload), so resolution always runs against the very list the index was built from - the two
+ // can never be paired with a different (for example, newer) provider list, and a change can never surface stale
+ // reference information.
+ internal sealed class ReferenceEngine
+ {
+ // A process-wide opt-out for apps that do not want configuration references at all. The value is a feature
+ // switch so the resolution paths can be trimmed away when set. Read as the outermost gate at every call site, so
+ // a set switch lets the trimmer drop the whole reference path.
+ [FeatureSwitchDefinition("Microsoft.Extensions.Configuration.DisableConfigurationReferences")]
+ internal static bool Disabled { get; } =
+ AppContext.TryGetSwitch("Microsoft.Extensions.Configuration.DisableConfigurationReferences", out bool disabled) && disabled;
+
+ private readonly object _lock = new object();
+
+ // Volatile so the lock-free fast-path read publishes the fully-built index (release on assignment, acquire on
+ // read) on weak memory models. Null means "not computed yet"; ReferenceIndex.Empty means "computed, nothing to
+ // resolve".
+ private volatile ReferenceIndex? _index;
+
+ private ReferenceEngine(IList providers) => Providers = providers;
+
+ public static ReferenceEngine? Create(IList providers) => Disabled ? null : new ReferenceEngine(providers);
+
+ public IList Providers { get; }
+
+ // The memoized reference index for this generation, built once from Providers. ReferenceIndex.Build returns the
+ // empty sentinel when no provider declares any $ref, so that "nothing to resolve" case lands on IsEmpty.
+ private ReferenceIndex Index()
+ {
+ ReferenceIndex? computed = _index;
+ if (computed is null)
+ {
+ lock (_lock)
+ {
+ computed = _index ??= ReferenceIndex.Build(Providers);
+ }
+ }
+
+ return computed;
+ }
+
+ // Whether this generation resolves nothing (no provider declares a $ref). Lets enumeration take the plain
+ // provider path - allocating like a reference-free configuration - instead of the merge path. The index is
+ // built (once, memoized) to answer this, which the first read of the generation does anyway.
+ internal bool IndexIsEmpty => Index().IsEmpty;
+
+ // Reads , applying recursive-merge reference resolution. The key is followed hop by hop
+ // through the reference chain; at each hop a provider strictly above the reference's declaring level may
+ // override the mirrored value at that exact path, otherwise the mirror is followed. When no reference governs
+ // the running key it is read across all providers. Returns whether a value was found.
+ public bool TryRead(string key, out string? value)
+ {
+ ReferenceIndex index = Index();
+ // A $ref key is structural metadata, not data: read it raw without resolving, so it neither redirects
+ // through its own parent's reference nor joins the walk. An empty index likewise means nothing resolves.
+ if (index.IsEmpty || ReferenceIndex.IsRefKey(key, out _))
+ {
+ // This generation holds no references (or the key is a marker), so read the key directly.
+ return TryGetAbove(key, minLevelExclusive: -1, out value);
+ }
+
+ string current = key;
+ CycleGuard guard = default;
+ while (index.TryGetGoverningRef(current, out string? target, out int level, out int prefixLength))
+ {
+ // A higher-precedence provider (strictly above the one that declared the reference) can override the
+ // mirrored value at this exact path; that override wins over following the reference.
+ if (TryGetAbove(current, level, out value))
+ {
+ return true;
+ }
+
+ guard.Advance(current, prefixLength);
+ current = prefixLength == current.Length
+ ? target
+ : target + current.Substring(prefixLength);
+ }
+
+ return TryGetAbove(current, minLevelExclusive: -1, out value);
+ }
+
+ // Collects the immediate child key segments of under recursive-merge resolution: the
+ // union, deduplicated, of the override children each higher provider defines at every reference hop and the
+ // mirror children of the final target.
+ public List ChildKeys(string? path)
+ {
+ ReferenceIndex index = Index();
+ var result = new List();
+ var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ string? current = path;
+ bool merged = false;
+ if (!index.IsEmpty)
+ {
+ // Only walk the reference chain when there is something to resolve; otherwise fall straight through to
+ // the plain enumeration of the path below.
+ CycleGuard guard = default;
+ while (current is not null && index.TryGetGoverningRef(current, out string? target, out int level, out int prefixLength))
+ {
+ AddChildKeysAbove(result, seen, current, level);
+ guard.Advance(current, prefixLength);
+ current = prefixLength == current.Length
+ ? target
+ : target + current.Substring(prefixLength);
+ merged = true;
+ }
+ }
+
+ AddChildKeysAbove(result, seen, current, minLevelExclusive: -1);
+
+ if (merged)
+ {
+ // A resolved section unions children drawn from several providers across the reference hops; each block
+ // arrives sorted on its own but the concatenation is not, so restore the ConfigurationKeyComparer order
+ // GetChildren guarantees. The plain (no-hop) path already comes out ordered from the providers, so it
+ // is left untouched.
+ result.Sort(ConfigurationKeyComparer.Comparison);
+ }
+
+ return result;
+ }
+
+ // Reads from the providers strictly above , highest
+ // precedence first, tolerating a provider disposed concurrently (as the plain read path does).
+ private bool TryGetAbove(string key, int minLevelExclusive, out string? value)
+ {
+ for (int i = Providers.Count - 1; i > minLevelExclusive; i--)
+ {
+ try
+ {
+ if (Providers[i].TryGet(key, out value))
+ {
+ return true;
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+
+ value = null;
+ return false;
+ }
+
+ // Adds the immediate child segments of drawn from the providers strictly above
+ // , skipping segments already collected and tolerating a disposed provider.
+ private void AddChildKeysAbove(List result, HashSet seen, string? path, int minLevelExclusive)
+ {
+ IEnumerable keys = Enumerable.Empty();
+ for (int i = minLevelExclusive + 1; i < Providers.Count; i++)
+ {
+ try
+ {
+ keys = Providers[i].GetChildKeys(keys, path);
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+
+ foreach (string key in keys)
+ {
+ // Hide the reserved $ref marker: it declares the reference, it is not a child of the section.
+ if (!ReferenceIndex.IsRefSegment(key) && seen.Add(key))
+ {
+ result.Add(key);
+ }
+ }
+ }
+
+ // Guards a single reference-resolution walk against a chain that never terminates. Every hop redirects through
+ // a governing reference key drawn from a finite set, so a walk that does not terminate - whether it repeats
+ // (A -> B -> A) or grows without bound (A -> A:B) - must fire some governing key twice, while a terminating
+ // walk fires each at most once. The first GraceHops hops are not recorded, so a chain that resolves quickly
+ // (the common case) allocates nothing; the fired-key set is created only once a walk outlives the grace period.
+ private struct CycleGuard
+ {
+ // A reference chain resolves in as many hops as it has links, a handful for any real configuration, so only
+ // a cycle or a pathologically deep chain outlasts this.
+ private const int GraceHops = 32;
+
+ private int _hops;
+ private HashSet? _fired;
+
+ public void Advance(string key, int prefixLength)
+ {
+ if (++_hops <= GraceHops)
+ {
+ return;
+ }
+
+ string governing = key.Substring(0, prefixLength);
+ if (!(_fired ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(governing))
+ {
+ throw new InvalidOperationException(
+ SR.Format(SR.Error_ReferenceCycle, string.Join(" -> ", _fired) + " -> " + governing));
+ }
+ }
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceIndex.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceIndex.cs
new file mode 100644
index 00000000000000..6ce275e730f1b6
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/ReferenceIndex.cs
@@ -0,0 +1,193 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Microsoft.Extensions.Configuration
+{
+ // A snapshot of every configuration path that is a reference, mapping the path to its immediate target. Built once
+ // per provider generation by scanning the providers (see Build). A reference is declared by a reserved "$ref" child
+ // key - ":$ref = " - so a plain value is never mistaken for a reference and no escaping is needed; the
+ // highest-precedence provider that declares a path's $ref wins, and an empty target drops it. Entries are kept
+ // unflattened - each maps to its immediate target - so resolution follows the chain hop by hop and can apply a
+ // higher provider's override at every hop; a chain that never terminates (a cycle, or a self-reference that grows
+ // without bound) is caught by the resolution walk itself (see ReferenceEngine.CycleGuard).
+ internal sealed class ReferenceIndex
+ {
+ public static readonly ReferenceIndex Empty = new ReferenceIndex(new Dictionary(StringComparer.OrdinalIgnoreCase));
+
+ // The reserved final segment that declares a reference: ":$ref = " makes a reference to
+ // . Because the marker is a key, not a value, values are always literal and never need escaping.
+ internal const string RefSegment = "$ref";
+
+ // Each reference key maps to its target and the level (provider index) of the provider that declared it. The
+ // level lets resolution honour a higher-precedence provider that overrides a mirrored value. Entries are kept
+ // unflattened: resolution follows the chain hop by hop so it can apply overrides at every hop.
+ private readonly Dictionary _targets;
+#if NET9_0_OR_GREATER
+ private readonly Dictionary.AlternateLookup> _bySpan;
+#endif
+
+ public ReferenceIndex(Dictionary targets)
+ {
+ _targets = targets;
+#if NET9_0_OR_GREATER
+ _bySpan = targets.GetAlternateLookup>();
+#endif
+ }
+
+ public bool IsEmpty => _targets.Count == 0;
+
+ // Finds the prefix of - including the key itself - that governs it, returning its
+ // , the (provider index) of the provider that declared it,
+ // and the length of the matched prefix. Among the prefixes that are references, the one declared by the
+ // highest-precedence provider governs; ties (references declared at the same level) are broken by the
+ // shallowest (outermost) prefix, so a nested reference under the same provider's alias is ignored while a more
+ // specific reference declared by a higher provider still wins over a lower provider's ancestor reference.
+ // Returns false when no prefix is a reference. Resolution walks these hop by hop (see ReferenceEngine.TryRead),
+ // applying a higher provider's override at each hop.
+ internal bool TryGetGoverningRef(string key, [NotNullWhen(true)] out string? target, out int level, out int prefixLength)
+ {
+ target = null;
+ level = -1;
+ prefixLength = 0;
+
+ if (_targets.Count != 0)
+ {
+ int start = 0;
+ while (true)
+ {
+ int delimiter = key.IndexOf(ConfigurationPath.KeyDelimiter[0], start);
+ int end = delimiter < 0 ? key.Length : delimiter;
+#if NET9_0_OR_GREATER
+ if (_bySpan.TryGetValue(key.AsSpan(0, end), out (string Target, int Level) entry) && entry.Level > level)
+#else
+ if (_targets.TryGetValue(key.Substring(0, end), out (string Target, int Level) entry) && entry.Level > level)
+#endif
+ {
+ // Only a strictly higher level replaces the running best, so on a tie the first (shallowest)
+ // match is kept.
+ target = entry.Target;
+ level = entry.Level;
+ prefixLength = end;
+ }
+
+ if (delimiter < 0)
+ {
+ break;
+ }
+ start = delimiter + 1;
+ }
+ }
+
+ return target is not null;
+ }
+
+ // Builds the index for a provider generation. Scans every provider (a ChainedConfigurationProvider resolves its
+ // own references and hides its inner providers, so it is skipped) from highest to lowest precedence for "$ref"
+ // keys. Each ":$ref = " records -> (, declaring level); the highest provider that
+ // declares a path's $ref wins (a lower provider's $ref for the same path is ignored) and an empty target drops
+ // the reference. Entries are kept unflattened so resolution can apply a higher provider's override at each hop
+ // of a chain; a chain that never terminates is caught later, by the resolution walk (see ReferenceEngine).
+ public static ReferenceIndex Build(IList providers)
+ {
+ Dictionary? targets = null;
+ HashSet? claimed = null;
+
+ for (int i = providers.Count - 1; i >= 0; i--)
+ {
+ IConfigurationProvider provider = providers[i];
+ if (provider is ChainedConfigurationProvider)
+ {
+ // A ChainedConfigurationProvider wraps a whole IConfiguration that already resolves its own
+ // references, and its merged view hides the inner providers. Its values are still read through the
+ // normal provider path, but it contributes none of its own references to this index.
+ continue;
+ }
+
+ foreach (KeyValuePair entry in ScanProvider(provider))
+ {
+ // The highest provider that declares a path's $ref decides it; claim the path here so a lower
+ // provider's $ref for the same path is ignored. An empty target claims the path but records no
+ // reference, so a higher provider can drop a lower reference by setting its $ref to empty.
+ if (IsRefKey(entry.Key, out string? referencePath)
+ && (claimed ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(referencePath)
+ && !string.IsNullOrEmpty(entry.Value))
+ {
+ (targets ??= new Dictionary(StringComparer.OrdinalIgnoreCase))[referencePath] = (entry.Value!, i);
+ }
+ }
+ }
+
+ return targets is null ? Empty : new ReferenceIndex(targets);
+ }
+
+ // The key/value pairs of a provider for the reference scan: a ConfigurationProvider exposes its loaded
+ // dictionary directly; any other provider is enumerated through GetChildKeys.
+ private static IEnumerable> ScanProvider(IConfigurationProvider provider)
+ => provider is ConfigurationProvider concrete ? concrete.GetEntriesForReferenceScan() : EnumerateProvider(provider);
+
+ // Walks every key of a non-scannable provider via GetChildKeys, collecting those that hold a value. A provider
+ // disposed mid-walk (concurrently, or after the ConfigurationManager itself was disposed) contributes whatever
+ // was collected before it threw, matching the plain read path that skips a provider throwing
+ // ObjectDisposedException. Collected eagerly because Build consumes the whole enumeration anyway.
+ private static List> EnumerateProvider(IConfigurationProvider provider)
+ {
+ var entries = new List>();
+ var pending = new Stack();
+ pending.Push(null);
+ var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ try
+ {
+ while (pending.Count > 0)
+ {
+ string? parent = pending.Pop();
+ foreach (string child in provider.GetChildKeys(Enumerable.Empty(), parent))
+ {
+ string childKey = parent is null ? child : parent + ConfigurationPath.KeyDelimiter + child;
+ if (!seen.Add(childKey))
+ {
+ continue;
+ }
+
+ if (provider.TryGet(childKey, out string? value))
+ {
+ entries.Add(new KeyValuePair(childKey, value));
+ }
+ pending.Push(childKey);
+ }
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+
+ return entries;
+ }
+
+ // Whether declares a reference - its final segment is the reserved "$ref" - and, if so,
+ // the it governs (the key without the trailing ":$ref"). A bare "$ref" at the
+ // root is not a reference, as there is no path for it to govern.
+ internal static bool IsRefKey(string key, [NotNullWhen(true)] out string? referencePath)
+ {
+ int suffixLength = ConfigurationPath.KeyDelimiter.Length + RefSegment.Length;
+ if (key.Length > suffixLength
+ && key[key.Length - suffixLength] == ConfigurationPath.KeyDelimiter[0]
+ && string.Compare(key, key.Length - RefSegment.Length, RefSegment, 0, RefSegment.Length, StringComparison.OrdinalIgnoreCase) == 0)
+ {
+ referencePath = key.Substring(0, key.Length - suffixLength);
+ return true;
+ }
+
+ referencePath = null;
+ return false;
+ }
+
+ // Whether a single key segment is the reserved "$ref" marker, so enumeration can hide it.
+ internal static bool IsRefSegment(string segment) => string.Equals(segment, RefSegment, StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Configuration/src/Resources/Strings.resx
index 4a1f5628be6829..047e7936737532 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/src/Resources/Strings.resx
+++ b/src/libraries/Microsoft.Extensions.Configuration/src/Resources/Strings.resx
@@ -129,4 +129,7 @@
Source.Stream cannot be null.
+
+ A configuration reference cycle was detected: {0}.
+
\ No newline at end of file
diff --git a/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationManagerTest.cs b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationManagerTest.cs
index e84f51b2c457fb..65ec48a11df6d2 100644
--- a/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationManagerTest.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationManagerTest.cs
@@ -1290,14 +1290,16 @@ public BlockReadOnMREProvider(ManualResetEventSlim mre, TimeSpan timeout)
public override bool TryGet(string key, out string? value)
{
- _readStartedTcs.SetResult(null);
+ // A single configuration read may probe the provider more than once (for example when references are
+ // resolved), so the "read started" latch must be idempotent.
+ _readStartedTcs.TrySetResult(null);
Assert.True(_mre.Wait(_timeout), "BlockReadOnMREProvider.TryGet() timed out.");
return base.TryGet(key, out value);
}
public override IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath)
{
- _readStartedTcs.SetResult(null);
+ _readStartedTcs.TrySetResult(null);
Assert.True(_mre.Wait(_timeout), "BlockReadOnMREProvider.GetChildKeys() timed out.");
return base.GetChildKeys(earlierKeys, parentPath);
}
diff --git a/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationReferenceTests.cs b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationReferenceTests.cs
new file mode 100644
index 00000000000000..b7b23c18108584
--- /dev/null
+++ b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationReferenceTests.cs
@@ -0,0 +1,1089 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Microsoft.DotNet.RemoteExecutor;
+using Microsoft.Extensions.Configuration.Memory;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.Extensions.Configuration.Test
+{
+ public class ConfigurationReferenceTests
+ {
+ public enum RootKind
+ {
+ Builder,
+ Manager,
+ }
+
+ public static IEnumerable