Skip to content

Commit 6681521

Browse files
StephenMolloyHongGit
authored andcommitted
Issue 4 (#6)
* Add 'tokenPattern' setting. * Test expand when matching empty tokens.
1 parent fd3dda7 commit 6681521

5 files changed

Lines changed: 157 additions & 51 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ msbuild.*
99
/packages/
1010
samples/SampleWebApp/bin/
1111
samples/SampleWebApp/obj/
12+
*.user

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ If you read the blog post linked above, you probably recognize that Configuratio
1010
concept to construct incredibly complex configuration on the fly. But for the most common usage scenarios, a simple key/value replacement mechanism is all that
1111
is needed. Most of the config builders in this project are such key/value builders.
1212

13+
#### mode
1314
The basic concept of these config builders is to draw on an external source of key/value information to populate parts of the config system that are key/value in
1415
nature. Specifically, the `appSettings` and `connectionStrings` sections receive special treatment from these key/value config builders. These builders can be
1516
set to run in three different modes:
@@ -22,6 +23,7 @@ set to run in three different modes:
2223
string. Any part of the raw xml string that matches the pattern __`${token}`__ is a candidate for token expansion. If no corresponding value is found in the
2324
external source, then the token is left alone.
2425

26+
#### prefix
2527
Another feature of these key/value Configuration Builders is prefix handling. Because full-framework .Net configuration is complex and nested, and external key/value
2628
sources are by nature quite simple and flat, leveraging key prefixes can be useful. For example, if you want to inject both App Settings and Connection Strings into
2729
your configuration via environment variables, you could accomplish this in two ways. Use the `EnvironmentConfigBuilder` in the default `Strict` mode and make sure you
@@ -41,10 +43,21 @@ so they can slurp up any setting or connection string you provide without needin
4143
```
4244
This way the same flat key/value source can be used to populate configuration for two different sections.
4345

44-
One final setting that is common among all of these key/value builders is `stripPrefix`. The code above does a good job of separating app settings from connection
46+
#### stripPrefix
47+
A related setting that is common among all of these key/value builders is `stripPrefix`. The code above does a good job of separating app settings from connection
4548
strings... but now all the keys in AppSettings start with "AppSetting_". Maybe this is fine for code you wrote. Chances are that prefix is better off stripped from the
4649
key name before being inserted into AppSettings. `stripPrefix` is a simple boolean value, and accomplishes just that. It's default value is `false`.
4750

51+
#### tokenPattern
52+
The final setting that is shared between all KeyValueConfigBuilder-derived builders is `tokenPattern`. When describing the `Expand` behavior of these builders
53+
above, it was mentioned that the raw xml is searched for tokens that look like __`${token}`__. This is done with a regular expression. `@"\$\{(\w+)\}"` to be exact.
54+
The set of characters that matches `\w` is more strict than xml and many sources of config values allow, and some applications may need to allow more exotic characters
55+
in their token names. Additionally there might be scenarios where the `${}` pattern is not acceptable.
56+
57+
`tokenPattern` allows developers to change the regex that is used for token matching. It is a simple string argument, and no validation is done to make sure it is
58+
a well-formed non-dangerous regex - so use it wisely. The only real restriction is that is must contain a capture group. The entire regex must match the entire token,
59+
and the first capture must be the token name to look up in the config source.
60+
4861
## Config Builders In This Project
4962

5063
### EnvironmentConfigBuilder

samples/SampleWebApp/SampleWebApp.csproj.user

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/Base/KeyValueConfigBuilder.cs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ public abstract class KeyValueConfigBuilder : ConfigurationBuilder
1515
public const string modeTag = "mode";
1616
public const string prefixTag = "prefix";
1717
public const string stripPrefixTag = "stripPrefix";
18+
public const string tokenPatternTag = "tokenPattern";
1819

1920
private bool _greedyInited;
2021
private IDictionary<string, string> _cachedValues;
2122
private bool _stripPrefix = false; // Prefix-stripping is all handled in this class; this is private so it doesn't confuse sub-classes.
2223

24+
public string TokenPattern { get; protected set; } = @"\$\{(\w+)\}";
2325
public KeyValueMode Mode { get; private set; } = KeyValueMode.Strict;
2426
public string KeyPrefix { get; private set; }
2527

@@ -32,18 +34,21 @@ public override void Initialize(string name, NameValueCollection config)
3234
{
3335
base.Initialize(name, config);
3436

35-
KeyPrefix = config?[prefixTag] ?? "";
36-
37-
if (config != null && config[stripPrefixTag] != null)
37+
// Override default config
38+
if (config != null)
3839
{
39-
// We want an exception here if 'stripPrefix' is specified but unrecognized.
40-
_stripPrefix = Boolean.Parse(config[stripPrefixTag]);
41-
}
40+
KeyPrefix = config[prefixTag] ?? "";
41+
TokenPattern = config[tokenPatternTag] ?? TokenPattern;
4242

43-
if (config != null && config[modeTag] != null)
44-
{
45-
// We want an exception here if 'mode' is specified but unrecognized.
46-
Mode = (KeyValueMode)Enum.Parse(typeof(KeyValueMode), config[modeTag], true);
43+
if (config[stripPrefixTag] != null) {
44+
// We want an exception here if 'stripPrefix' is specified but unrecognized.
45+
_stripPrefix = Boolean.Parse(config[stripPrefixTag]);
46+
}
47+
48+
if (config[modeTag] != null) {
49+
// We want an exception here if 'mode' is specified but unrecognized.
50+
Mode = (KeyValueMode)Enum.Parse(typeof(KeyValueMode), config[modeTag], true);
51+
}
4752
}
4853

4954
_cachedValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -115,7 +120,7 @@ private XmlNode ExpandTokens(XmlNode rawXml)
115120
if (String.IsNullOrEmpty(rawXmlString))
116121
return rawXml;
117122

118-
rawXmlString = Regex.Replace(rawXmlString, @"\$\{(\w+)\}", (m) =>
123+
rawXmlString = Regex.Replace(rawXmlString, TokenPattern, (m) =>
119124
{
120125
string key = m.Groups[1].Value;
121126

test/Microsoft.Configuration.ConfigurationBuilders.Test/Test/BaseTests.cs

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public void BaseParameters_Prefix()
7171

7272
// Case sensitive attribute name
7373
builder = new FakeConfigBuilder();
74-
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "prefix", "$This_is_my_other_PREFIX#" } });
74+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "PREfix", "$This_is_my_other_PREFIX#" } });
7575
Assert.AreEqual(builder.KeyPrefix, "$This_is_my_other_PREFIX#");
7676
}
7777

@@ -114,6 +114,31 @@ public void BaseParameters_StripPrefix()
114114
Assert.AreEqual(builder.StripPrefix, true);
115115
}
116116

117+
[TestMethod]
118+
public void BaseParameters_TokenPattern()
119+
{
120+
// Default string. (Not null)
121+
var builder = new FakeConfigBuilder();
122+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection());
123+
Assert.AreEqual(builder.TokenPattern, @"\$\{(\w+)\}");
124+
125+
// TokenPattern, case preserved
126+
builder = new FakeConfigBuilder();
127+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "tokenPattern", @"%([^\s+\W*#$&_-])}%" } });
128+
Assert.AreEqual(builder.TokenPattern, @"%([^\s+\W*#$&_-])}%");
129+
130+
// Case sensitive attribute name
131+
builder = new FakeConfigBuilder();
132+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "TOKenpaTTerN", @"\[pattern\]" } });
133+
Assert.AreEqual(builder.TokenPattern, @"\[pattern\]");
134+
135+
// Protected setter
136+
builder = new FakeConfigBuilder();
137+
builder.Initialize("test", null);
138+
builder.SetTokenPattern("TestPattern");
139+
Assert.AreEqual(builder.TokenPattern, @"TestPattern");
140+
}
141+
117142
// ======================================================================
118143
// Behaviors
119144
// ======================================================================
@@ -297,6 +322,96 @@ public void BaseBehavior_Expand()
297322
Assert.IsNull(GetValueFromXml(xmlOutput, "Prefix_TestKey1"));
298323
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey2"));
299324
Assert.IsNull(GetValueFromXml(xmlOutput, "${TestKey1}"));
325+
326+
// Expand - ProcessRawXml with alternate tokenPattern
327+
builder = new FakeConfigBuilder();
328+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "mode", "Expand" }, { "tokenPattern", @"%%([\w:]+)%%" } });
329+
xmlInput = GetNode(rawXmlInput);
330+
xmlOutput = builder.ProcessRawXml(xmlInput);
331+
Assert.AreEqual("appSettings", xmlOutput.Name);
332+
Assert.AreEqual("val1", GetValueFromXml(xmlOutput, "TestKey1"));
333+
Assert.AreEqual("${TestKey1}", GetValueFromXml(xmlOutput, "test1"));
334+
Assert.AreEqual("expandTestValue", GetValueFromXml(xmlOutput, "${TestKey1}"));
335+
Assert.AreEqual("PrefixTest1", GetValueFromXml(xmlOutput, "TestKey"));
336+
Assert.AreEqual("PrefixTest2", GetValueFromXml(xmlOutput, "Prefix_TestKey"));
337+
Assert.AreEqual("${Prefix_TestKey1}", GetValueFromXml(xmlOutput, "PreTest2"));
338+
Assert.AreEqual("ThisWasAnAlternateTokenPattern", GetValueFromXml(xmlOutput, "AltTokenTest"));
339+
Assert.AreEqual("ThisWasAnAltTokenPatternWithPrefix", GetValueFromXml(xmlOutput, "AltTokenTest2"));
340+
Assert.IsNull(GetValueFromXml(xmlOutput, "Prefix_TestKey1"));
341+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey2"));
342+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey1Value"));
343+
344+
// Expand - ProcessRawXml does not work with alternate tokenPattern with no capture group
345+
builder = new FakeConfigBuilder();
346+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "mode", "Expand" }, { "tokenPattern", @"%%[\w:]+%%" } });
347+
xmlInput = GetNode(rawXmlInput);
348+
xmlOutput = builder.ProcessRawXml(xmlInput);
349+
Assert.AreEqual("appSettings", xmlOutput.Name);
350+
Assert.AreEqual("val1", GetValueFromXml(xmlOutput, "TestKey1"));
351+
Assert.AreEqual("${TestKey1}", GetValueFromXml(xmlOutput, "test1"));
352+
Assert.AreEqual("expandTestValue", GetValueFromXml(xmlOutput, "${TestKey1}"));
353+
Assert.AreEqual("PrefixTest1", GetValueFromXml(xmlOutput, "TestKey"));
354+
Assert.AreEqual("PrefixTest2", GetValueFromXml(xmlOutput, "Prefix_TestKey"));
355+
Assert.AreEqual("${Prefix_TestKey1}", GetValueFromXml(xmlOutput, "PreTest2"));
356+
Assert.AreEqual("%%Alt:Token%%", GetValueFromXml(xmlOutput, "AltTokenTest"));
357+
Assert.AreEqual("%%Prefix_Alt:Token%%", GetValueFromXml(xmlOutput, "AltTokenTest2"));
358+
Assert.IsNull(GetValueFromXml(xmlOutput, "Prefix_TestKey1"));
359+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey2"));
360+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey1Value"));
361+
362+
// Expand - ProcessRawXml does not blow up with alternate tokenPattern with empty capture group
363+
builder = new FakeConfigBuilder();
364+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "mode", "Expand" }, { "tokenPattern", @"%(.?)%" } });
365+
xmlInput = GetNode(rawXmlInput);
366+
xmlOutput = builder.ProcessRawXml(xmlInput);
367+
Assert.AreEqual("appSettings", xmlOutput.Name);
368+
Assert.AreEqual("val1", GetValueFromXml(xmlOutput, "TestKey1"));
369+
Assert.AreEqual("${TestKey1}", GetValueFromXml(xmlOutput, "test1"));
370+
Assert.AreEqual("expandTestValue", GetValueFromXml(xmlOutput, "${TestKey1}"));
371+
Assert.AreEqual("PrefixTest1", GetValueFromXml(xmlOutput, "TestKey"));
372+
Assert.AreEqual("PrefixTest2", GetValueFromXml(xmlOutput, "Prefix_TestKey"));
373+
Assert.AreEqual("${Prefix_TestKey1}", GetValueFromXml(xmlOutput, "PreTest2"));
374+
Assert.AreEqual("%%Alt:Token%%", GetValueFromXml(xmlOutput, "AltTokenTest"));
375+
Assert.AreEqual("%%Prefix_Alt:Token%%", GetValueFromXml(xmlOutput, "AltTokenTest2"));
376+
Assert.IsNull(GetValueFromXml(xmlOutput, "Prefix_TestKey1"));
377+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey2"));
378+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey1Value"));
379+
380+
// Expand - ProcessRawXml with alternate tokenPattern and prefix
381+
builder = new FakeConfigBuilder();
382+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "mode", "Expand" }, { "tokenPattern", @"%%([\w:]+)%%" }, { "prefix", "Prefix_" } });
383+
xmlInput = GetNode(rawXmlInput);
384+
xmlOutput = builder.ProcessRawXml(xmlInput);
385+
Assert.AreEqual("appSettings", xmlOutput.Name);
386+
Assert.AreEqual("val1", GetValueFromXml(xmlOutput, "TestKey1"));
387+
Assert.AreEqual("${TestKey1}", GetValueFromXml(xmlOutput, "test1"));
388+
Assert.AreEqual("expandTestValue", GetValueFromXml(xmlOutput, "${TestKey1}"));
389+
Assert.AreEqual("PrefixTest1", GetValueFromXml(xmlOutput, "TestKey"));
390+
Assert.AreEqual("PrefixTest2", GetValueFromXml(xmlOutput, "Prefix_TestKey"));
391+
Assert.AreEqual("${Prefix_TestKey1}", GetValueFromXml(xmlOutput, "PreTest2"));
392+
Assert.AreEqual("%%Alt:Token%%", GetValueFromXml(xmlOutput, "AltTokenTest"));
393+
Assert.AreEqual("ThisWasAnAltTokenPatternWithPrefix", GetValueFromXml(xmlOutput, "AltTokenTest2"));
394+
Assert.IsNull(GetValueFromXml(xmlOutput, "Prefix_TestKey1"));
395+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey2"));
396+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey1Value"));
397+
398+
// Expand - ProcessRawXml with alternate tokenPattern and strip prefix
399+
builder = new FakeConfigBuilder();
400+
builder.Initialize("test", new System.Collections.Specialized.NameValueCollection() { { "mode", "Expand" }, { "tokenPattern", @"%%([\w:]+)%%" }, { "prefix", "Prefix_" }, { "stripPrefix", "true" } });
401+
xmlInput = GetNode(rawXmlInput);
402+
xmlOutput = builder.ProcessRawXml(xmlInput);
403+
Assert.AreEqual("appSettings", xmlOutput.Name);
404+
Assert.AreEqual("val1", GetValueFromXml(xmlOutput, "TestKey1"));
405+
Assert.AreEqual("${TestKey1}", GetValueFromXml(xmlOutput, "test1"));
406+
Assert.AreEqual("expandTestValue", GetValueFromXml(xmlOutput, "${TestKey1}"));
407+
Assert.AreEqual("PrefixTest1", GetValueFromXml(xmlOutput, "TestKey"));
408+
Assert.AreEqual("PrefixTest2", GetValueFromXml(xmlOutput, "Prefix_TestKey"));
409+
Assert.AreEqual("${Prefix_TestKey1}", GetValueFromXml(xmlOutput, "PreTest2"));
410+
Assert.AreEqual("ThisWasAnAltTokenPatternWithPrefix", GetValueFromXml(xmlOutput, "AltTokenTest"));
411+
Assert.AreEqual("%%Prefix_Alt:Token%%", GetValueFromXml(xmlOutput, "AltTokenTest2"));
412+
Assert.IsNull(GetValueFromXml(xmlOutput, "Prefix_TestKey1"));
413+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey2"));
414+
Assert.IsNull(GetValueFromXml(xmlOutput, "TestKey1Value"));
300415
}
301416

302417
[TestMethod]
@@ -470,6 +585,8 @@ public void BaseErrors_ProcessConfigurationSection()
470585
<add key=""TestKey"" value=""PrefixTest1"" />
471586
<add key=""Prefix_TestKey"" value=""PrefixTest2"" />
472587
<add key=""PreTest2"" value=""${Prefix_TestKey1}"" />
588+
<add key=""AltTokenTest"" value=""%%Alt:Token%%"" />
589+
<add key=""AltTokenTest2"" value=""%%Prefix_Alt:Token%%"" />
473590
</appSettings>";
474591

475592
XmlNode GetNode(string xmlInput)
@@ -510,7 +627,9 @@ class FakeConfigBuilder : KeyValueConfigBuilder
510627
{ "TestKey1", "TestKey1Value" },
511628
{ "TestKey2", "TestKey2Value" },
512629
{ "Prefix_TestKey", "Prefix_TestKeyValue" },
513-
{ "Prefix_TestKey1", "Prefix_TestKey1Value" }
630+
{ "Prefix_TestKey1", "Prefix_TestKey1Value" },
631+
{ "Alt:Token", "ThisWasAnAlternateTokenPattern" },
632+
{ "Prefix_Alt:Token", "ThisWasAnAltTokenPatternWithPrefix" }
514633
};
515634

516635
public bool FailInit = false;
@@ -549,5 +668,10 @@ public override ICollection<KeyValuePair<string, string>> GetAllValues(string pr
549668

550669
return sourceValues.Where(s => s.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
551670
}
671+
672+
public void SetTokenPattern(string newPattern)
673+
{
674+
this.TokenPattern = newPattern;
675+
}
552676
}
553677
}

0 commit comments

Comments
 (0)