diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs b/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs index 8951697d8d68a..4e0393da281d5 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs @@ -22,13 +22,11 @@ internal static IEnumerable GetChildrenImplementation(thi { using ReferenceCountedProviders? reference = (root as ConfigurationManager)?.GetProvidersReference(); IEnumerable providers = reference?.Providers ?? root.Providers; - IEnumerable children = providers - .Aggregate(Enumerable.Empty(), - (seed, source) => source.GetChildKeys(seed, path)) + .SelectMany(p => p.GetChildKeys(Enumerable.Empty(), path)) + .OrderBy(key => key, ConfigurationKeyComparer.Instance) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key))); - if (reference is null) { return children; diff --git a/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs index 7d0df82f58561..7e0348a365074 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs @@ -971,5 +971,130 @@ public void ProviderWithNullReloadToken() // Assert Assert.NotNull(config); } + + [Fact] + public void ConfigurationGetChildrenKeysOrdered() + { + // Arrange + var dic1 = new Dictionary() + { + {"Services:2", "Service2"} + }; + var dic2 = new Dictionary() + { + {"Services:10", "Service10"}, + }; + var memConfigSrc1 = new MemoryConfigurationSource { InitialData = dic1 }; + var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.Add(memConfigSrc1); + configurationBuilder.Add(memConfigSrc2); + + var config = configurationBuilder.Build(); + + // Act + var keys = config.GetSection("Services").GetChildren().Select(c => c.Key).ToList(); + + // Assert + Assert.Equal(2, keys.Count); + Assert.Equal("2", keys[0]); + Assert.Equal("10", keys[1]); + } + + [Theory] + [ClassData(typeof(OverrideGetChildKeysConfigurationProviders))] + public void ConfigurationGetChildrenNotOptimizedConfigurationProvider(IConfigurationProvider provider) + { + // Arrange + var dic2 = new Dictionary() + { + {"key2", "val"}, + }; + var src1 = new FixedProviderConfigurationSource(provider); + var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.Add(src1); + configurationBuilder.Add(memConfigSrc2); + var configSorted = configurationBuilder.Build(); + + // Act + var keysSorted = configSorted.GetChildren().Select(c => c.Key).ToList(); + + // Assert + Assert.Equal(3, keysSorted.Count); + + // The keys should be sorted by the 2nd IConfigurationProvider + // because it inherits from helper class ConfigurationProvider. + Assert.Equal("key1", keysSorted[0]); + Assert.Equal("key2", keysSorted[1]); + Assert.Equal("key3", keysSorted[2]); + } + + private class OverrideGetChildKeysConfigurationProviders : TheoryData + { + public OverrideGetChildKeysConfigurationProviders() + { + Add(new InheritHelperClassButOverrideMethodGetChildKeysConfigurationProvider()); + Add(new NotInheritHelperClassConfigurationProvider()); + } + + public static IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) + { + if (parentPath != null) + { + throw new ArgumentException("Not support parentPath"); + } + + foreach (var key in earlierKeys) + { + yield return key; + } + + // For the IConfigurationProvider implementation that not inherit from helper class ConfigurationProvider, + // the returned keys might not be sorted. + yield return "key3"; + yield return "key1"; + } + } + + private class FixedProviderConfigurationSource : IConfigurationSource + { + private readonly IConfigurationProvider provider; + public FixedProviderConfigurationSource(IConfigurationProvider provider) + { + this.provider = provider; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) => provider; + } + + private class InheritHelperClassButOverrideMethodGetChildKeysConfigurationProvider : ConfigurationProvider + { + public override IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) + => OverrideGetChildKeysConfigurationProviders.GetChildKeys(earlierKeys, parentPath); + } + + private class NotInheritHelperClassConfigurationProvider : IConfigurationProvider + { + public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) + => OverrideGetChildKeysConfigurationProviders.GetChildKeys(earlierKeys, parentPath); + + public Primitives.IChangeToken GetReloadToken() => new ConfigurationReloadToken(); + + public void Load() + { + } + + public void Set(string key, string? value) + { + } + + public bool TryGet(string key, out string? value) + { + value = "val"; + return true; + } + } } }