diff --git a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationKeyComparer.cs b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationKeyComparer.cs index 00cca781d47f46..f00a9ab49f041d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationKeyComparer.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationKeyComparer.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Configuration /// public class ConfigurationKeyComparer : IComparer { - private static readonly string[] _keyDelimiterArray = new[] { ConfigurationPath.KeyDelimiter }; + private const char KeyDelimiter = ':'; /// /// The default instance. @@ -29,29 +29,61 @@ public class ConfigurationKeyComparer : IComparer /// Less than 0 if x is less than y, 0 if x is equal to y and greater than 0 if x is greater than y. public int Compare(string? x, string? y) { - string[] xParts = x?.Split(_keyDelimiterArray, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); - string[] yParts = y?.Split(_keyDelimiterArray, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); + ReadOnlySpan xSpan = x.AsSpan(); + ReadOnlySpan ySpan = y.AsSpan(); + + xSpan = SkipAheadOnDelimiter(xSpan); + ySpan = SkipAheadOnDelimiter(ySpan); // Compare each part until we get two parts that are not equal - for (int i = 0; i < Math.Min(xParts.Length, yParts.Length); i++) + while (!xSpan.IsEmpty && !ySpan.IsEmpty) { - x = xParts[i]; - y = yParts[i]; + int xDelimiterIndex = xSpan.IndexOf(KeyDelimiter); + int yDelimiterIndex = ySpan.IndexOf(KeyDelimiter); + + int compareResult = Compare( + xDelimiterIndex == -1 ? xSpan : xSpan.Slice(0, xDelimiterIndex), + yDelimiterIndex == -1 ? ySpan : ySpan.Slice(0, yDelimiterIndex)); + + if (compareResult != 0) + { + return compareResult; + } - int value1 = 0; - int value2 = 0; + xSpan = xDelimiterIndex == -1 ? default : + SkipAheadOnDelimiter(xSpan.Slice(xDelimiterIndex + 1)); + ySpan = yDelimiterIndex == -1 ? default : + SkipAheadOnDelimiter(ySpan.Slice(yDelimiterIndex + 1)); + } - bool xIsInt = x != null && int.TryParse(x, out value1); - bool yIsInt = y != null && int.TryParse(y, out value2); + return xSpan.IsEmpty ? (ySpan.IsEmpty ? 0 : -1) : 1; + static ReadOnlySpan SkipAheadOnDelimiter(ReadOnlySpan a) + { + while (!a.IsEmpty && a[0] == KeyDelimiter) + { + a = a.Slice(1); + } + return a; + } + + static int Compare(ReadOnlySpan a, ReadOnlySpan b) + { +#if NETCOREAPP + bool aIsInt = int.TryParse(a, out int value1); + bool bIsInt = int.TryParse(b, out int value2); +#else + bool aIsInt = int.TryParse(a.ToString(), out int value1); + bool bIsInt = int.TryParse(b.ToString(), out int value2); +#endif int result; - if (!xIsInt && !yIsInt) + if (!aIsInt && !bIsInt) { // Both are strings - result = string.Compare(x, y, StringComparison.OrdinalIgnoreCase); + result = a.CompareTo(b, StringComparison.OrdinalIgnoreCase); } - else if (xIsInt && yIsInt) + else if (aIsInt && bIsInt) { // Both are int result = value1 - value2; @@ -59,19 +91,11 @@ public int Compare(string? x, string? y) else { // Only one of them is int - result = xIsInt ? -1 : 1; + result = aIsInt ? -1 : 1; } - if (result != 0) - { - // One of them is different - return result; - } + return result; } - - // If we get here, the common parts are equal. - // If they are of the same length, then they are totally identical - return xParts.Length - yParts.Length; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationPathComparerTest.cs b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationPathComparerTest.cs index 159ba1d4a75cdf..a61cc806176036 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationPathComparerTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationPathComparerTest.cs @@ -14,6 +14,8 @@ public void CompareWithNull() ComparerTest(null, null, 0); ComparerTest(null, "a", -1); ComparerTest("b", null, 1); + ComparerTest(null, "a:b", -1); + ComparerTest(null, "a:b:c", -1); } [Fact] @@ -32,6 +34,20 @@ public void CompareWithDifferentLengths() ComparerTest("aa", "a", 1); } + [Fact] + public void CompareWithEmpty() + { + ComparerTest(":", "", 0); + ComparerTest(":", "::", 0); + ComparerTest(null, "", 0); + ComparerTest(":", null, 0); + ComparerTest("::", null, 0); + ComparerTest(" : : ", null, 1); + ComparerTest("b: :a", "b::a", -1); + ComparerTest("b:\t:a", "b::a", -1); + ComparerTest("b::a: ", "b::a:", 1); + } + [Fact] public void CompareWithLetters() { diff --git a/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs index 70a1abf218a10d..7d0df82f585618 100644 --- a/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration/tests/ConfigurationTest.cs @@ -59,6 +59,64 @@ public void LoadAndCombineKeyValuePairsFromDifferentConfigurationProviders() Assert.Null(config["NotExist"]); } + [Fact] + private void GetChildKeys_CanChainEmptyKeys() + { + var input = new Dictionary() { }; + for (int i = 0; i < 1000; i++) + { + input.Add(new string(' ', i), string.Empty); + } + + IConfigurationRoot configurationRoot = new ConfigurationBuilder() + .Add(new MemoryConfigurationSource + { + InitialData = input + }) + .Build(); + + var chainedConfigurationSource = new ChainedConfigurationSource + { + Configuration = configurationRoot, + ShouldDisposeConfiguration = false, + }; + + var chainedConfiguration = new ChainedConfigurationProvider(chainedConfigurationSource); + IEnumerable childKeys = chainedConfiguration.GetChildKeys(new string[0], null); + Assert.Equal(1000, childKeys.Count()); + Assert.Equal(string.Empty, childKeys.First()); + Assert.Equal(999, childKeys.Last().Length); + } + + [Fact] + private void GetChildKeys_CanChainKeyWithNoDelimiter() + { + var input = new Dictionary() { }; + for (int i = 1000; i < 2000; i++) + { + input.Add(i.ToString(), string.Empty); + } + + IConfigurationRoot configurationRoot = new ConfigurationBuilder() + .Add(new MemoryConfigurationSource + { + InitialData = input + }) + .Build(); + + var chainedConfigurationSource = new ChainedConfigurationSource + { + Configuration = configurationRoot, + ShouldDisposeConfiguration = false, + }; + + var chainedConfiguration = new ChainedConfigurationProvider(chainedConfigurationSource); + IEnumerable childKeys = chainedConfiguration.GetChildKeys(new string[0], null); + Assert.Equal(1000, childKeys.Count()); + Assert.Equal("1000", childKeys.First()); + Assert.Equal("1999", childKeys.Last()); + } + [Fact] public void CanChainConfiguration() {