Skip to content

Commit 6d2f310

Browse files
Don't lose case in Greedy substitutions. (#91)
1 parent 38109c3 commit 6d2f310

4 files changed

Lines changed: 64 additions & 4 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ public class MySpecialSectionHandler : SectionHandler<MySpecialSection>
298298
{
299299
// T ConfigSection;
300300
// public override void Initialize(string name, NameValueCollection config) {}
301+
// public override string TryGetOriginalCase(string requestedKey) {}
301302
302303
public override IEnumerator<KeyValuePair<string, object>> GetEnumerator() {}
303304

@@ -306,7 +307,7 @@ public class MySpecialSectionHandler : SectionHandler<MySpecialSection>
306307
```
307308
Keep in mind when implementing a section handler, that `InsertOrUpdate()` will be called while iterating over the enumerator
308309
supplied by `GetEnumerator()`. So the two methods must work in cooperation to make sure that the enumerator does not get confused
309-
while iterating.
310+
while iterating. Ie, don't tamper with the collection that the enumerator is iterating over.
310311

311312
A section handler is free to interpret the structure of a `ConfigurationSection` in any way it sees fit, so long it can be exposed as an
312313
enumerable list of key/value things. The 'value' side of that pair doesn't even have to be a string. Consider the implementation of
@@ -318,6 +319,12 @@ New section handlers can be introduced to the config system... via config. Secti
318319
provider model introduced in .Net 2.0, so they require `name` and `type` attributes, but can additionally support any other
319320
attribute needed by passing them in a `NameValueCollection` to the `Initialize(name, config)` method.
320321

322+
Section handlers also have an optional override method `TryGetOriginalCase` that attempts to preserve casing in config files
323+
when executing in `Greedy` mode. When operating in `Strict` mode, config builders in this repo always preserved the case of the
324+
key when updating the value. This was easy because the original key was already at hand during lookup and replacement. In `Greedy`
325+
mode however, the original key from the config file was not used since it was not needed for a one-item lookup. Thus any greedy substitutions
326+
had their keys replaced with the keys from the external config source. Functionally they should be the same, since key/value config
327+
is supposed to be case-insensitive. But aesthetically, being a good citizen and not replacing the original key case is good.
321328

322329
The `AppSettingsSectionHandler` and `ConnectionStringsSectionHandler`
323330
are implicitly added at the root level config, but they can be clear/removed just like any other item in an add/remove/clear configuration

samples/SampleWebApp/Web.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494

9595
<appConfigTest configBuilders="appconfig1,appconfig2,appconfig3">
9696
<add key="acTest" value="Will be replaced by appconfig1. Should be 'test1'"/>
97-
<add key="acTEST2" value="Should be case-insensitive replaced by appconfig1, and again by appconfig2. Should be 'test2b'"/>
97+
<add key="acTEST2" value="Should be case-insensitive-and-preserve replaced by appconfig1, and again by appconfig2. Should be 'test2b'"/>
9898
<!-- <add key="acTest3" value="Will be added by appconfig2. Should be 'test3b'" /> -->
9999
<!-- <add key="acTest4" value="Will not exist." /> -->
100100
<add key="acLMTest" value="Will be replaced by appconfig3. Should be 'oldest'"/>

src/Base/KeyValueConfigBuilder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,9 @@ public override ConfigurationSection ProcessConfigurationSection(ConfigurationSe
251251
{
252252
foreach (var configItem in handler)
253253
{
254-
string newValue = GetValueInternal(configItem.Key);
254+
// Presumably, UpdateKey will preserve casing appropriately, so newKey is cased as expected.
255255
string newKey = UpdateKey(configItem.Key);
256+
string newValue = GetValueInternal(configItem.Key);
256257

257258
if (newValue != null)
258259
handler.InsertOrUpdate(newKey, newValue, configItem.Key, configItem.Value);
@@ -267,8 +268,9 @@ public override ConfigurationSection ProcessConfigurationSection(ConfigurationSe
267268
{
268269
if (kvp.Value != null)
269270
{
271+
// Here, kvp.Key is not from the config file, so it might not be correctly cased. Get the correct casing for UpdateKey.
270272
string oldKey = TrimPrefix(kvp.Key);
271-
string newKey = UpdateKey(oldKey);
273+
string newKey = UpdateKey(handler.TryGetOriginalCase(oldKey));
272274
handler.InsertOrUpdate(newKey, kvp.Value, oldKey);
273275
}
274276
}

src/Base/SectionHandler.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Configuration.Provider;
77
using System.Collections.Specialized;
88
using System;
9+
using System.Linq.Expressions;
910

1011
namespace Microsoft.Configuration.ConfigurationBuilders
1112
{
@@ -14,6 +15,7 @@ internal interface ISectionHandler
1415
{
1516
void InsertOrUpdate(string newKey, string newValue, string oldKey = null, object oldItem = null);
1617
IEnumerator<KeyValuePair<string, object>> GetEnumerator();
18+
string TryGetOriginalCase(string requestedKey);
1719
}
1820

1921
/// <summary>
@@ -45,6 +47,17 @@ public abstract class SectionHandler<T> : ProviderBase, ISectionHandler where T
4547
/// <param name="oldItem">A reference to the old key/value pair obtained by <see cref="GetEnumerator"/>, or null.</param>
4648
public abstract void InsertOrUpdate(string newKey, string newValue, string oldKey = null, object oldItem = null);
4749

50+
/// <summary>
51+
/// Attempt to lookup the original key casing so it can be preserved during greedy updates which would otherwise lose
52+
/// the original casing in favor of the casing used in the config source.
53+
/// </summary>
54+
/// <param name="requestedKey">The key to find original casing for.</param>
55+
/// <returns>Unless overridden, returns the string passed in.</returns>
56+
public virtual string TryGetOriginalCase(string requestedKey)
57+
{
58+
return requestedKey;
59+
}
60+
4861
private void Initialize(string name, T configSection, NameValueCollection config)
4962
{
5063
ConfigSection = configSection;
@@ -88,6 +101,25 @@ public override IEnumerator<KeyValuePair<string, object>> GetEnumerator()
88101
foreach (string key in keys)
89102
yield return new KeyValuePair<string, object>(key, key);
90103
}
104+
105+
/// <summary>
106+
/// Attempt to lookup the original key casing so it can be preserved during greedy updates which would otherwise lose
107+
/// the original casing in favor of the casing used in the config source.
108+
/// </summary>
109+
/// <param name="requestedKey">The key to find original casing for.</param>
110+
/// <returns>A string containing the key with original casing from the config section, or the key as passed in if no match
111+
/// can be found.</returns>
112+
public override string TryGetOriginalCase(string requestedKey)
113+
{
114+
if (!String.IsNullOrWhiteSpace(requestedKey))
115+
{
116+
var keyval = ConfigSection.Settings[requestedKey];
117+
if (keyval != null)
118+
return keyval.Key;
119+
}
120+
121+
return base.TryGetOriginalCase(requestedKey);
122+
}
91123
}
92124

93125
/// <summary>
@@ -136,5 +168,24 @@ public override IEnumerator<KeyValuePair<string, object>> GetEnumerator()
136168
foreach (ConnectionStringSettings cs in connStrs)
137169
yield return new KeyValuePair<string, object>(cs.Name, cs);
138170
}
171+
172+
/// <summary>
173+
/// Attempt to lookup the original key casing so it can be preserved during greedy updates which would otherwise lose
174+
/// the original casing in favor of the casing used in the config source.
175+
/// </summary>
176+
/// <param name="requestedKey">The key to find original casing for.</param>
177+
/// <returns>A string containing the key with original casing from the config section, or the key as passed in if no match
178+
/// can be found.</returns>
179+
public override string TryGetOriginalCase(string requestedKey)
180+
{
181+
if (!String.IsNullOrWhiteSpace(requestedKey))
182+
{
183+
var connStr = ConfigSection.ConnectionStrings[requestedKey];
184+
if (connStr != null)
185+
return connStr.Name;
186+
}
187+
188+
return base.TryGetOriginalCase(requestedKey);
189+
}
139190
}
140191
}

0 commit comments

Comments
 (0)