Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Microsoft.Extensions.Configuration.IConfigurationSection> 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<System.Collections.Generic.KeyValuePair<string, string?>>? 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<Microsoft.Extensions.Configuration.IConfigurationSource> sources, Microsoft.Extensions.Configuration.ReferenceMode mode) { throw null; }
}
public abstract partial class StreamConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider
{
public StreamConfigurationProvider(Microsoft.Extensions.Configuration.StreamConfigurationSource source) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,17 @@ public IConfigurationRoot Build()
var providers = new List<IConfigurationProvider>();
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<IConfigurationProvider, ReferenceMode>? providerModes = ReferenceResolutionConfigurationBuilderExtensions
.ResolveProviderModes(Properties, _sources, providers);
engine = new ReferenceResolutionEngine(providers, providerModes);
}
return new ConfigurationRoot(providers, engine);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public sealed class ConfigurationManager : IConfigurationManager, IConfiguration
private readonly List<IDisposable> _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;
Comment on lines +40 to +44
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment references opting in via EnableReferenceResolution, but the public API introduced in this PR is SetReferenceMode(..., ReferenceMode.Scan) (and tests use a shim). Update the comment to avoid implying a non-existent public entry point.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Creates an empty mutable configuration object that is both an <see cref="IConfigurationBuilder"/> and an <see cref="IConfigurationRoot"/>.
/// </summary>
Expand All @@ -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
Expand All @@ -64,6 +76,8 @@ public string? this[string key]
}
}

internal ReferenceResolutionEngine? Engine => _engine;

/// <inheritdoc/>
public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);

Expand All @@ -84,6 +98,7 @@ public string? this[string key]
public void Dispose()
{
DisposeRegistrations();
Interlocked.Exchange(ref _engine, null)?.Dispose();
_providerManager.Dispose();
}

Expand All @@ -109,6 +124,7 @@ void IConfigurationRoot.Reload()
}
}

_engine?.Invalidate();
RaiseChanged();
}

Expand All @@ -129,6 +145,7 @@ private void AddSource(IConfigurationSource source)
_changeTokenRegistrations.Add(ChangeToken.OnChange(provider.GetReloadToken, RaiseChanged));

_providerManager.AddProvider(provider);
SwapEngine();
RaiseChanged();
}

Expand All @@ -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<IConfigurationProvider> providers = _providerManager.GetProvidersSnapshot();
Dictionary<IConfigurationProvider, ReferenceMode>? 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
Expand Down Expand Up @@ -276,7 +314,14 @@ public object this[string key]
set
{
_properties[key] = value;
_config.ReloadSources();
if (IsReferenceResolutionProperty(key))
{
_config.SwapEngine();
}
else
{
_config.ReloadSources();
}
}
}

Expand All @@ -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<string, object> item)
{
((IDictionary<string, object>)_properties).Add(item);
_config.ReloadSources();
if (IsReferenceResolutionProperty(item.Key))
{
_config.SwapEngine();
}
else
{
_config.ReloadSources();
}
}

public void Clear()
Expand Down Expand Up @@ -329,17 +388,43 @@ public IEnumerator<KeyValuePair<string, object>> 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<string, object> item)
{
var wasRemoved = ((IDictionary<string, object>)_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);
Expand All @@ -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<string, object> Raw => _properties;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace Microsoft.Extensions.Configuration
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
private readonly IList<IConfigurationProvider> _providers;
private readonly ReferenceResolutionEngine? _engine;
private readonly List<IDisposable> _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();

Expand All @@ -25,10 +26,16 @@ public class ConfigurationRoot : IConfigurationRoot, IDisposable
/// </summary>
/// <param name="providers">The <see cref="IConfigurationProvider"/>s for this configuration.</param>
public ConfigurationRoot(IList<IConfigurationProvider> providers)
: this(providers, engine: null)
{
}

internal ConfigurationRoot(IList<IConfigurationProvider> providers, ReferenceResolutionEngine? engine)
{
ArgumentNullException.ThrowIfNull(providers);

_providers = providers;
_engine = engine;
_changeTokenRegistrations = new List<IDisposable>(providers.Count);
foreach (IConfigurationProvider p in providers)
{
Expand All @@ -42,14 +49,24 @@ public ConfigurationRoot(IList<IConfigurationProvider> providers)
/// </summary>
public IEnumerable<IConfigurationProvider> Providers => _providers;

internal ReferenceResolutionEngine? Engine => _engine;

/// <summary>
/// Gets or sets the value corresponding to a configuration key.
/// </summary>
/// <param name="key">The configuration key.</param>
/// <returns>The configuration value.</returns>
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);
}

Expand Down Expand Up @@ -86,6 +103,7 @@ public void Reload()
{
provider.Load();
}
_engine?.Invalidate();
RaiseChanged();
}

Expand All @@ -109,6 +127,8 @@ public void Dispose()
{
(provider as IDisposable)?.Dispose();
}

_engine?.Dispose();
}

internal static string? GetConfiguration(IList<IConfigurationProvider> providers, string key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(thi
using ReferenceCountedProviders? reference = (root as ConfigurationManager)?.GetProvidersReference();
IEnumerable<IConfigurationProvider> providers = reference?.Providers ?? root.Providers;

IEnumerable<IConfigurationSection> children = providers
IEnumerable<string> keys = providers
.Aggregate(Enumerable.Empty<string>(),
(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<IConfigurationSection> children = keys
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(key => root.GetSection(path == null ? key : path + ConfigurationPath.KeyDelimiter + key));

Expand All @@ -42,6 +50,18 @@ internal static IEnumerable<IConfigurationSection> 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<IConfigurationProvider> in ConfigurationRoot
IList<IConfigurationProvider> providers = root.Providers is IList<IConfigurationProvider> list
? list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment mentions ReferenceResolutionConfigurationSource, but there is no such type in this project (searching the repo only finds this comment). Please update/remove the reference so the comment matches the actual implementation using ReferenceResolutionEngine on the root.

Suggested change
// source.Build(...) loop so a ReferenceResolutionConfigurationSource can observe providers
// source.Build(...) loop so the root's ReferenceResolutionEngine can observe providers

Copilot uses AI. Check for mistakes.
// registered before it.
public IReadOnlyList<IConfigurationProvider> GetProvidersSnapshot()
{
lock (_replaceProvidersLock)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ConfigurationManager));
}

return _refCountedProviders.Providers.ToArray();
}
}

public void Dispose()
{
ReferenceCountedProviders oldRefCountedProviders = _refCountedProviders;
Expand Down
Loading
Loading