Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class ChainedConfigurationProvider : IConfigurationProvider, IDisposable
{
private readonly IConfiguration _config;
private readonly bool _shouldDisposeConfig;
private bool _initialLoadCompleted;

/// <summary>
/// Initializes a new instance from the source configuration.
Expand Down Expand Up @@ -60,7 +61,24 @@ public bool TryGet(string key, out string? value)
/// <summary>
/// Loads configuration values from the source represented by this <see cref="IConfigurationProvider"/>.
/// </summary>
public void Load() { }
public void Load()
{
if (!_initialLoadCompleted)
{
// The initial load is a no-op since the chained configuration is expected to be already loaded by the
// time it is used as a source for another configuration. This way we avoid unnecessary change notifications.
_initialLoadCompleted = true;
return;
}

if (_config is IConfigurationRoot root)
{
foreach (IConfigurationProvider provider in root.Providers)
{
provider.Load();
}
}
}

/// <summary>
/// Returns the immediate descendant configuration keys for a given parent path based on the data of this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Configuration.Memory;
using Xunit;
Expand Down Expand Up @@ -54,10 +55,132 @@ public void ChainedConfiguration_ExposesProvider()
Assert.Equal(providers, configRoot.Providers);
}

[Fact]
public void ChainedConfiguration_ReloadPropagatesToInnerConfigurationRoot()
{
var innerConfig = new ConfigurationBuilder()
.Add(new CountingValueConfigurationSource())
.Build();

var outerConfig = new ConfigurationBuilder()
.AddConfiguration(innerConfig)
.Build();

Assert.Equal("1", outerConfig["SomeValue"]);

outerConfig.Reload();

Assert.Equal("2", outerConfig["SomeValue"]);
}

[Fact]
public void ChainedConfiguration_ReloadDoesNotPropagateToInnerConfigurationSection()
{
var innerConfig = new ConfigurationBuilder()
.Add(new CountingValueConfigurationSource("Section:SomeValue"))
.Build();

var outerConfig = new ConfigurationBuilder()
.AddConfiguration(innerConfig.GetSection("Section"))
.Build();

Assert.Equal("1", outerConfig["SomeValue"]);

outerConfig.Reload();

Assert.Equal("1", outerConfig["SomeValue"]);
}

[Fact]
public void ChainedConfiguration_BuildingOuterConfigurationRoot_DoesNotReloadInnerConfigurationRoot()
{
var innerProvider = new CountingValueConfigurationProvider("Value");
var innerConfig = new ConfigurationRoot(new[] { innerProvider });

int notifications = 0;
innerConfig.GetReloadToken().RegisterChangeCallback(_ => notifications++, state: null);

var outerConfig = new ConfigurationBuilder()
.AddConfiguration(innerConfig)
.Build();

Assert.Equal(1, innerProvider.LoadCount);
Assert.Equal("1", innerConfig["Value"]);
Assert.Equal("1", outerConfig["Value"]);
Assert.Equal(0, notifications);
}

[Fact]
public void ChainedConfiguration_AddingToConfigurationManager_DoesNotReloadInnerConfigurationRoot()
{
var innerProvider = new CountingValueConfigurationProvider("Value");
var innerConfig = new ConfigurationRoot(new[] { innerProvider });

int notifications = 0;
innerConfig.GetReloadToken().RegisterChangeCallback(_ => notifications++, state: null);

var outerConfig = new ConfigurationManager();
outerConfig.AddConfiguration(innerConfig);

Assert.Equal(1, innerProvider.LoadCount);
Assert.Equal("1", innerConfig["Value"]);
Assert.Equal("1", outerConfig["Value"]);
Assert.Equal(0, notifications);
}

[Fact]
public void ChainedConfiguration_ReloadingOuterConfigurationRoot_RaisesSingleOuterNotificationAndNoInnerNotification()
{
var innerProvider = new CountingValueConfigurationProvider("Value");
var innerConfig = new ConfigurationRoot(new[] { innerProvider });

var outerConfig = new ConfigurationBuilder()
.AddConfiguration(innerConfig)
.Build();

int innerNotifications = 0;
int outerNotifications = 0;

innerConfig.GetReloadToken().RegisterChangeCallback(_ => innerNotifications++, state: null);
outerConfig.GetReloadToken().RegisterChangeCallback(_ => outerNotifications++, state: null);

outerConfig.Reload();

Assert.Equal(2, innerProvider.LoadCount);
Assert.Equal("2", innerConfig["Value"]);
Assert.Equal("2", outerConfig["Value"]);
Assert.Equal(1, outerNotifications);
Assert.Equal(0, innerNotifications);
}

private class TestConfigurationProvider : ConfigurationProvider
{
public TestConfigurationProvider(string key, string value)
=> Data.Add(key, value);
}

private class CountingValueConfigurationSource : IConfigurationSource
{
private readonly string _key;

public CountingValueConfigurationSource(string key = "SomeValue")
=> _key = key;

public IConfigurationProvider Build(IConfigurationBuilder builder)
=> new CountingValueConfigurationProvider(_key);
}

private class CountingValueConfigurationProvider : ConfigurationProvider
{
private readonly string _key;

public CountingValueConfigurationProvider(string key)
=> _key = key;

public int LoadCount { get; private set; }

public override void Load()
=> Data[_key] = (++LoadCount).ToString(CultureInfo.InvariantCulture);
}
}
}
Loading