Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Makes GetChildKeys more efficient #67186

Merged
merged 14 commits into from
Mar 31, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Configuration
/// </summary>
public class ConfigurationKeyComparer : IComparer<string>
{
private static readonly string[] _keyDelimiterArray = new[] { ConfigurationPath.KeyDelimiter };
private const char KeyDelimiter = ':';

/// <summary>
/// The default instance.
Expand All @@ -29,49 +29,73 @@ public class ConfigurationKeyComparer : IComparer<string>
/// <returns>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.</returns>
public int Compare(string? x, string? y)
{
string[] xParts = x?.Split(_keyDelimiterArray, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
string[] yParts = y?.Split(_keyDelimiterArray, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
ReadOnlySpan<char> xSpan = x.AsSpan();
ReadOnlySpan<char> 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<char> SkipAheadOnDelimiter(ReadOnlySpan<char> a)
{
while (!a.IsEmpty && a[0] == KeyDelimiter)
{
a = a.Slice(1);
}
return a;
}

static int Compare(ReadOnlySpan<char> a, ReadOnlySpan<char> 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;
}
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,64 @@ public void LoadAndCombineKeyValuePairsFromDifferentConfigurationProviders()
Assert.Null(config["NotExist"]);
}

[Fact]
private void GetChildKeys_CanChainEmptyKeys()
{
var input = new Dictionary<string, string>() { };
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<string> 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<string, string>() { };
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<string> 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()
{
Expand Down