Skip to content

Commit 465ca55

Browse files
author
Dean Ward
authored
Configuration: Fix GetChildKeys to work correctly in prefixed configuration provider (#19)
Prior to this commit `GetChildKeys` was using a naive implementation that did not function correctly when there were multiple `IConfigurationProvider` implementations chained to the prefixed configuration. This commit updates the method to ensure that it - returns the prefix and earlier keys if the parent path is null - returns just the earlier keys if the parent path is not null and does not have the prefix - correctly passes the parent path *without* the prefix to the child configuration providers when the parent path is prefixed It also adds a test to validate that the fixes are sane. Finally, in a fun episode of Yak Shaving, this tweaks `global.json` and `Directory.Build.props` to ensure that the project can build - there's an issue with upgrading the latest stable VS to 16.8 that inadvertently (and incorrectly) nukes SDK directories and our global.json was preventing the use of the 5.0 SDK for build purposes. And then Nerdbank got angry so needed an upgrade too. Yay!
1 parent 75e5737 commit 465ca55

File tree

4 files changed

+69
-18
lines changed

4 files changed

+69
-18
lines changed

global.json

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
{
22
"sdk": {
3-
"version": "3.1.300",
4-
"rollForward": "latestMinor",
53
"allowPrerelease": false
64
}
75
}

src/Directory.Build.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</PropertyGroup>
2121

2222
<ItemGroup>
23-
<PackageReference Include="Nerdbank.GitVersioning" Version="3.0.28" PrivateAssets="all" />
23+
<PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="all" />
2424
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
2525
</ItemGroup>
2626
</Project>
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Linq;
44
using Microsoft.Extensions.Configuration;
@@ -8,49 +8,71 @@ namespace StackExchange.Utils
88
internal class PrefixedConfigurationProvider : CompositeConfigurationProvider
99
{
1010
private readonly string _prefixWithDelimiter;
11+
private readonly string _prefix;
12+
1113
public const char Delimiter = ':';
1214

1315
public PrefixedConfigurationProvider(string prefix, IConfigurationRoot configurationRoot) : base(configurationRoot)
1416
{
17+
_prefix = prefix;
1518
_prefixWithDelimiter = prefix + Delimiter;
1619
}
1720

1821
public override IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath)
1922
{
20-
var earlierKeyList = earlierKeys.ToList();
21-
foreach (var provider in ConfigurationRoot.Providers)
23+
if (parentPath != null && !parentPath.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase))
24+
{
25+
return earlierKeys;
26+
}
27+
28+
var keys = new List<string>();
29+
if (parentPath == null)
30+
{
31+
keys.Add(_prefix);
32+
}
33+
else
2234
{
23-
foreach (var childKey in provider.GetChildKeys(earlierKeyList, parentPath))
35+
string parentPathWithoutPrefix = null;
36+
if (!parentPath.Equals(_prefix, StringComparison.OrdinalIgnoreCase))
2437
{
25-
yield return _prefixWithDelimiter + childKey;
38+
parentPathWithoutPrefix = WithoutPrefix(parentPath);
39+
}
40+
41+
foreach (var provider in ConfigurationRoot.Providers)
42+
{
43+
foreach (var childKey in provider.GetChildKeys(keys, parentPathWithoutPrefix))
44+
{
45+
keys.Add(childKey);
46+
}
2647
}
2748
}
49+
50+
return keys.Concat(earlierKeys).OrderBy(k => k, ConfigurationKeyComparer.Instance);
2851
}
2952

3053
public override void Set(string key, string value)
3154
{
32-
if (!key.StartsWith(_prefixWithDelimiter) || key.Length == _prefixWithDelimiter.Length)
55+
if (!key.StartsWith(_prefixWithDelimiter, StringComparison.OrdinalIgnoreCase) || key.Length == _prefixWithDelimiter.Length)
3356
{
3457
return;
3558
}
3659

37-
// TODO: make this moar efficient!
38-
// slice off the prefix so we can fetch from our underlying providers
39-
base.Set(key.AsSpan().Slice(_prefixWithDelimiter.Length).ToString(), value);
60+
base.Set(WithoutPrefix(key), value);
4061
}
4162

4263
public override bool TryGet(string key, out string value)
4364
{
44-
if (!key.StartsWith(_prefixWithDelimiter) || key.Length == _prefixWithDelimiter.Length)
65+
if (!key.StartsWith(_prefixWithDelimiter, StringComparison.OrdinalIgnoreCase) || key.Length == _prefixWithDelimiter.Length)
4566
{
4667
value = null;
4768
return false;
4869
}
4970

50-
// TODO: make this moar efficient!
51-
// slice off the prefix so we can fetch from our underlying providers
52-
var keyWithoutPrefix = key.AsSpan().Slice(_prefixWithDelimiter.Length);
53-
return base.TryGet(keyWithoutPrefix.ToString(), out value);
71+
return base.TryGet(WithoutPrefix(key), out value);
5472
}
73+
74+
// TODO: make this moar efficient!
75+
// slice off the prefix so we can fetch from our underlying providers
76+
private string WithoutPrefix(string path) => path == null ? path : path.AsSpan().Slice(_prefixWithDelimiter.Length).ToString();
5577
}
5678
}

tests/StackExchange.Utils.Tests/PrefixedConfigurationProviderTests.cs

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using Microsoft.Extensions.Configuration;
44
using Xunit;
@@ -23,6 +23,37 @@ public void InvalidArguments()
2323
);
2424
}
2525

26+
[Fact]
27+
public void PrefixesDoNotTrashPreviousKeys()
28+
{
29+
var configuration = new ConfigurationBuilder()
30+
.AddInMemoryCollection(
31+
new Dictionary<string, string>
32+
{
33+
["Kestrel:Endpoints:Http:Url"] = "http://*:6001/",
34+
["Testing:Blah"] = "BaseValue"
35+
}
36+
)
37+
.WithPrefix(
38+
"secrets",
39+
c =>
40+
{
41+
c.AddInMemoryCollection(
42+
new Dictionary<string, string>
43+
{
44+
["Testing:Blah"] = "ShouldNotOverride",
45+
["ShouldBeAccessibleUsingPrefix"] = "Test"
46+
});
47+
})
48+
.Build();
49+
50+
Assert.Equal("http://*:6001/", configuration.GetValue<string>("Kestrel:Endpoints:Http:Url"));
51+
Assert.Equal("BaseValue", configuration.GetValue<string>("Testing:Blah"));
52+
Assert.Equal("ShouldNotOverride", configuration.GetValue<string>("secrets:Testing:Blah"));
53+
Assert.Equal("Test", configuration.GetValue<string>("secrets:ShouldBeAccessibleUsingPrefix"));
54+
Assert.Null(configuration.GetValue<string>("ShouldBeAccessibleUsingPrefix"));
55+
}
56+
2657
[Fact]
2758
public void ValuesArePrefixed()
2859
{

0 commit comments

Comments
 (0)