Skip to content

Commit 1bdc238

Browse files
Add ConnectionStringSectionHandler2 (#211)
1 parent e72f1ea commit 1bdc238

12 files changed

Lines changed: 592 additions & 3 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ linked here:
3939
* Character Mapping - Some config builders have had an internal mapping of characters that might exist in keys in the config file but are illegal in keys at the
4040
source. As more scenarios come to light and individual prefrences are not always unanimous, V3 instead adds the [`charMap`](docs/KeyValueConfigBuilders.md#charmap) attribute to allow this character
4141
mapping to work with all **KeyValueConfigBuilders** and to be handled in an easily configurable manner.
42+
* `ConnectionStringsSectionHandler2` - A new section handler for the `<connectionStrings>` section has been included in the base package. This new handler will
43+
allow updating of both the 'connectionString' attribute as well as the 'providerName' attribute. It does require the builders and source of config data to be
44+
aware of this new ability though. The default section handler for the `<connectionStrings>` section has not been updated and remains as it was in previous
45+
versions, so apps wishing to take advantage of the new handler will have to wire it up in their config. More details can be found in the
46+
[SectionHandlers documentation](docs/SetionHandlers.md#ConnectionStringsSectionHandler2).
4247

4348
### V2 Updates:
4449
* Azure App Configuration Support - There is a [new builder](docs/KeyValueConfigBuilders.md#azureappconfigurationbuilder) for drawing values from the new Azure App Configuration service.

docs/SectionHandlers.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,36 @@ compiled in a separate assembly in the 'bin' directory. For example:
8686
</handlers>
8787
</Microsoft.Configuration.ConfigurationBuilders.SectionHandlers>
8888
```
89+
90+
## ConnectionStringsSectionHandler2
91+
Version 3 of this package suite includes a new section handler for the `<connectionStrings>` section. For maximum compatibility, the
92+
old section handler is still used by default. Here we will explain how to use this new section handler and how to replace the old one.
93+
94+
The new `ConnectionStringsSectionHandler2` works pretty much like it's non-numbered predecessor. The main difference is, when it
95+
enumerates through the list of connection strings in the section, it will ask builders to look for values that match
96+
"&lt;name&gt;:connectionString" and "&lt;name&gt;:providerName" in addition to just "&lt;name&gt;". When updating or inserting
97+
new `ConnectionStringSettings` items into the configuration collection, values that come from tagged keys will go to the
98+
appropriate attribute. Values that are found without a tagged key update the 'connectionString' property as they did before.
99+
100+
```xml
101+
<connectionStrings configBuilders="StrictBuilder">
102+
<add name="strict-cs" connectionString="Will get the value of 'strict-cs' or 'strict-cs:connectionString'"
103+
providerName="Will only get the value of 'strict-cs:providerName'" />
104+
105+
<!-- Easy to imagine pulling these from a structured json file. -->
106+
<add name="token-cs1" connectionString="${tokenCS:connectionString}"
107+
providerName="${tokenCS:providerName}" />
108+
<!-- But token mode can be messy. -->
109+
<add name="token-cs2" connectionString="${token-names-not-important}"
110+
providerName="${they-can-even-be-tagged-wrong:connectionString}" />
111+
</connectionStrings>
112+
```
113+
114+
One thing to note is that the mechanism for associating a key/value lookup with a specific attribute of connection string entries
115+
is a simple post-fix to the key. This makes this feature incompatible with versioned keys in Azure Key Vault. Most other cases should
116+
just work as they did before, with a little extra magic cleanliness when you start using this feature.
117+
118+
An example of this new behavior from `ConnectionStringsSectionHandler2` can be seen in the [SampleConsoleApp](samples/SampleConsoleApp/App.config#L37-L46).
119+
Or in the [test project](https://github.com/aspnet/MicrosoftConfigurationBuilders/blob/main/test/Microsoft.Configuration.ConfigurationBuilders.Test/ConnectionStringsSectionHandler2Tests.cs).
120+
The [SampleWebApp](samples/SampleWebApp/Web.config#L38-L41) does not use the new section handler, but it does include some notes about how
121+
it's resulting connection string collection would look different if it did.

samples/SampleConsoleApp/App.config

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
<add name="Env" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment" />
1717
<add name="KeyPerFile" mode="Greedy" directoryPath="${SampleItems}/KeyPerFileRoot" type="Microsoft.Configuration.ConfigurationBuilders.KeyPerFileConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.KeyPerFile" />
1818
<add name="Json" mode="Greedy" jsonMode="Flat" jsonFile="${jsonFile}" type="Microsoft.Configuration.ConfigurationBuilders.SimpleJsonConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Json" />
19+
<add name="JsonCS" mode="Strict" jsonMode="Sectional" jsonFile="${jsonFile}" type="Microsoft.Configuration.ConfigurationBuilders.SimpleJsonConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Json" />
1920
<add name="JsonExpand" mode="Token" jsonMode="Sectional" jsonFile="${jsonFile}" type="SamplesLib.ExpandWrapper`1[[Microsoft.Configuration.ConfigurationBuilders.SimpleJsonConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Json]], SamplesLib" />
2021
</builders>
2122
</configBuilders>
2223

2324
<Microsoft.Configuration.ConfigurationBuilders.SectionHandlers>
2425
<handlers>
26+
<remove name="DefaultConnectionStringsHandler" />
27+
<add name="NewConnectionStringsHandler" type="Microsoft.Configuration.ConfigurationBuilders.ConnectionStringsSectionHandler2, Microsoft.Configuration.ConfigurationBuilders.Base" />
2528
<add name="ClientSettingsHandler" type="SamplesLib.ClientSettingsSectionHandler, SamplesLib" />
2629
</handlers>
2730
</Microsoft.Configuration.ConfigurationBuilders.SectionHandlers>
@@ -31,6 +34,17 @@
3134
<add key="jsonFile" value="~/../../../SampleWebApp/App_Data/settings.json" />
3235
</appSettings>
3336

37+
<connectionStrings configBuilders="JsonCS">
38+
<add name="simpleCS" connectionString="A Simple Connection String" providerName="A Simple Provider Name" />
39+
<add name="noProvider" connectionString="A Connection String with no providerName" />
40+
<add name="jsonConnectionString1" connectionString="" />
41+
<!-- In Strict/Greedy mode, this advanced example updates both attributes as expected. -->
42+
<!-- In Token mode however, note that only the 'tagged' token works (and it is applied to the wrong attribute here for clear demonstration)
43+
because there is no 'jsonAdvConnStr' config value. If there was, the 'connectionString' attribute would be updated in addition
44+
to 'providerName.'-->
45+
<add name="jsonAdvConnStr" connectionString="${jsonAdvConnStr}" providerName="${jsonAdvConnStr:connectionString}"/>
46+
</connectionStrings>
47+
3448
<customSettings configBuilders="Json" />
3549

3650
<expandedSettings configBuilders="JsonExpand">

samples/SampleConsoleApp/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ static void Main(string[] args)
1717
Console.WriteLine("");
1818
Console.WriteLine("");
1919

20+
Console.WriteLine("---------- Connection Strings (Advanced Sample) ----------");
21+
Console.WriteLine("Name ConnectionString ProviderName");
22+
Console.WriteLine("------------------------+-------------------------------------------------+--------------------------------------------------");
23+
foreach (ConnectionStringSettings cs in ConfigurationManager.ConnectionStrings)
24+
{
25+
Console.WriteLine($"{cs.Name.PadRight(25)}{cs.ConnectionString.PadRight(50)}{cs.ProviderName}");
26+
}
27+
28+
Console.WriteLine("");
29+
Console.WriteLine("");
30+
2031
Console.WriteLine("---------- Custom Settings ----------");
2132
var customSettings = ConfigurationManager.GetSection("customSettings") as NameValueCollection;
2233
foreach (string setting in customSettings.Keys)

samples/SampleWebApp/App_Data/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
"jsonConnectionString1": "CS1 From ConnectionStrings Json object",
1515
"specialFeature": {
1616
"connStr": "Special feature CS From ConnectionStrings Json object"
17+
},
18+
"jsonAdvConnStr": {
19+
"connectionString": "Advanced ConnectionString",
20+
"providerName": "Advanced ProviderName"
1721
}
1822
},
1923

samples/SampleWebApp/Web.config

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<add name="Secrets" mode="Token" userSecretsFile="${SampleItems}/secrets.xml" type="Microsoft.Configuration.ConfigurationBuilders.UserSecretsConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.UserSecrets" />
1818
<add name="KeyVault" mode="Greedy" prefix="AppSettings_" stripPrefix="true" vaultName="${KeyVaultTestName}" enabled="${AzureBuildersEnabled}" type="Microsoft.Configuration.ConfigurationBuilders.AzureKeyVaultConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Azure" />
1919
<add name="AzConfig" mode="Token" tokenPattern="\[(\w+)\]" endpoint="[AzConfigTestEndpoint]" enabled="[AzureBuildersEnabled]" type="Microsoft.Configuration.ConfigurationBuilders.AzureAppConfigurationBuilder, Microsoft.Configuration.ConfigurationBuilders.AzureAppConfiguration" />
20+
<add name="Json2" mode="Token" jsonMode="Sectional" jsonFile="${jsonFile}" type="Microsoft.Configuration.ConfigurationBuilders.SimpleJsonConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Json" />
2021
</builders>
2122
</configBuilders>
2223

@@ -31,9 +32,13 @@
3132
<add key="AzConfigTestEndpoint" value="This comes from an environment variable." />
3233
</appSettings>
3334

34-
<connectionStrings configBuilders="Secrets,Json">
35+
<connectionStrings configBuilders="Secrets,Json,Json2">
3536
<add name="connStrFromSecrets" connectionString="${ConnectionString1}" providerName="MyDataProvider" />
3637
<add name="connectionString1" connectionString="Does not come from secrets in 'Token' mode." />
38+
<!-- 'Json2' will replace the connectionString on this in 'Token' mode, or both the CS and providerName in the old 'Expand' mode. -->
39+
<!-- Note that both of the jsonAdvConnStr:* values also show up as their own connection strings as a result of the 'Greedy' 'Json' builder.
40+
With the new ConnectionStringsSectionHandler2, they would be consolodated into one coherent 'jsonAdvConnStr' entry. -->
41+
<add name="jsonTricks-for-Token-or-Expand" connectionString="${jsonAdvConnStr:connectionString}" providerName="${jsonAdvConnStr:providerName}"/>
3742
</connectionStrings>
3843

3944
<customSettings configBuilders="KeyPerFile,AzConfig" />

samples/SampleWebApp/default.aspx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
}
1515
Response.Write("</table><br/><br/>");
1616
17-
Response.Write("<table border=1><tr><th colspan=2><h2>Connection Strings</h2></th></tr>");
17+
Response.Write("<table border=1><tr><th colspan=3><h2>Connection Strings</h2></th></tr>");
1818
foreach (ConnectionStringSettings cs in WebConfigurationManager.ConnectionStrings) {
19-
Response.Write("<tr><td>" + HttpUtility.HtmlEncode(cs.Name) + "</td><td>" + HttpUtility.HtmlEncode(cs.ConnectionString) + "</td></tr>");
19+
Response.Write("<tr><td>" + HttpUtility.HtmlEncode(cs.Name) + "</td><td>" + HttpUtility.HtmlEncode(cs.ConnectionString) + "</td><td>" + HttpUtility.HtmlEncode(cs.ProviderName) + "</td></tr>");
2020
}
2121
Response.Write("</table><br/><br/>");
2222

src/Base/SectionHandler.cs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,120 @@ public override string TryGetOriginalCase(string requestedKey)
212212
return base.TryGetOriginalCase(requestedKey);
213213
}
214214
}
215+
216+
/// <summary>
217+
/// A class that can be used by <see cref="KeyValueConfigBuilder"/>s to apply key/value config pairs to <see cref="ConnectionStringsSection"/>
218+
/// with special 'tagging' to allow updating both the 'connectionString' attribute as well as the 'providerName' attribute.
219+
/// </summary>
220+
public class ConnectionStringsSectionHandler2 : SectionHandler<ConnectionStringsSection>
221+
{
222+
private const string connStrNameTag = ":connectionString";
223+
private const string providerNameTag = ":providerName";
224+
225+
private class CSSH2State { public bool UpdateName; public ConnectionStringSettings CS; }
226+
227+
/// <summary>
228+
/// Updates an existing connection string attribute in the assigned <see cref="SectionHandler{T}.ConfigSection"/> with a new name and a new value. The old
229+
/// connection string setting can be located using the <paramref name="oldItem"/> parameter. If an old connection string is not found, a new connection
230+
/// string should be inserted.
231+
/// </summary>
232+
/// <param name="newKey">The updated key name for the connection string. May be post-fixed with attribute tag.</param>
233+
/// <param name="newValue">The updated value for the connection string.</param>
234+
/// <param name="oldKey">The old key name for the connection string, or null. May be post-fixed with attribute tag.</param>
235+
/// <param name="oldItem">A reference to the old <see cref="ConnectionStringSettings"/> object obtained by <see cref="KeysValuesAndState"/>, or null.</param>
236+
public override void InsertOrUpdate(string newKey, string newValue, string oldKey = null, object oldItem = null)
237+
{
238+
string tag;
239+
(oldKey, tag) = SplitTag(oldKey);
240+
(newKey, _) = SplitTag(newKey);
241+
242+
CSSH2State state = oldItem as CSSH2State;
243+
ConnectionStringSettings cs = state?.CS ?? ConfigSection.ConnectionStrings[oldKey] ?? new ConnectionStringSettings();
244+
245+
// Make sure there are no entries using the old or new name other than this one
246+
ConfigSection.ConnectionStrings.Remove(oldKey);
247+
ConfigSection.ConnectionStrings.Remove(newKey);
248+
249+
// Update values and re-add to the collection (no state means 'Greedy' mode where we do want to update)
250+
if (state == null || state.UpdateName)
251+
cs.Name = newKey;
252+
if (tag == providerNameTag)
253+
cs.ProviderName = newValue;
254+
else
255+
cs.ConnectionString = newValue;
256+
ConfigSection.ConnectionStrings.Add(cs);
257+
}
258+
259+
/// <summary>
260+
/// Gets an <see cref="IEnumerable{T}"/> for iterating over the key/value pairs contained in the assigned <see cref="SectionHandler{T}.ConfigSection"/>. />
261+
/// </summary>
262+
/// <returns>An enumerator over tuples where the values of the tuple are the existing name for each connection string, the value of
263+
/// the connection string or the value of the provider name, and a reference to the <see cref="ConnectionStringSettings"/> object itself
264+
/// which will be returned to us as a reference state object when updating the config record.</returns>
265+
public override IEnumerable<Tuple<string, string, object>> KeysValuesAndState()
266+
{
267+
// The ConnectionStrings collection may change on us while we enumerate. :/
268+
ConnectionStringSettings[] connStrs = new ConnectionStringSettings[ConfigSection.ConnectionStrings.Count];
269+
ConfigSection.ConnectionStrings.CopyTo(connStrs, 0);
270+
271+
foreach (ConnectionStringSettings cs in connStrs)
272+
{
273+
// Greedy mode doesn't enumerate here. It just goes direct to 'InsertOrUpdate', which preserves non-tagged
274+
// behavior via null-tag awareness.
275+
// Strict mode will need us to lookup a non-tagged value in addition to tagged values in order to
276+
// remain as compatible as possible with the simple old model.
277+
// Token mode is trickier. See step-by-step notes.
278+
279+
string originalName = cs.Name;
280+
string originalCS = cs.ConnectionString;
281+
282+
// In 'Token' mode, this will replace tokens in 'name' and 'connectionString'.
283+
yield return Tuple.Create(originalName, originalCS, (object)new CSSH2State() { UpdateName = true, CS = cs }) ;
284+
285+
// In 'Token' mode, this will re-replace tokens in 'connectionString' only. Conceptually a no-op, except we
286+
// don't know which mode we're in so we can't technically skip this re-replacement. We also can't skip this step because
287+
// it is required for 'Strict' mode. (It will also re-lookup tokens in 'name', but we are able to skip replacing those
288+
// here, since using _this_ tagged 'name' string might not be faithful to the original non-tagged 'name'.)
289+
// Also, re-lookups for tokens should be cached and free, since the tokens inside the 'name' didn't change when tagged.
290+
yield return Tuple.Create(originalName + connStrNameTag, originalCS, (object)new CSSH2State() { UpdateName = false, CS = cs });
291+
292+
// In 'Token' mode, this will replace tokens in 'providerName' only. Same deal with 'name' as the previous step. However,
293+
// the tag on the original name is important, as that is the only way we will know to work on 'providerName' instead of 'name'
294+
// in 'InsertOrUpdate'.
295+
yield return Tuple.Create(originalName + providerNameTag, cs.ProviderName, (object)new CSSH2State() { UpdateName = false, CS = cs });
296+
}
297+
}
298+
299+
/// <summary>
300+
/// Attempt to lookup the original key casing so it can be preserved during greedy updates which would otherwise lose
301+
/// the original casing in favor of the casing used in the config source.
302+
/// </summary>
303+
/// <param name="requestedKey">The key to find original casing for.</param>
304+
/// <returns>A string containing the key with original casing from the config section, or the key as passed in if no match
305+
/// can be found.</returns>
306+
public override string TryGetOriginalCase(string requestedKey)
307+
{
308+
if (!String.IsNullOrWhiteSpace(requestedKey))
309+
{
310+
var connStr = ConfigSection.ConnectionStrings[requestedKey];
311+
if (connStr != null)
312+
return connStr.Name;
313+
}
314+
315+
return base.TryGetOriginalCase(requestedKey);
316+
}
317+
318+
private (string, string) SplitTag(string key)
319+
{
320+
if (key != null)
321+
{
322+
if (key.EndsWith(connStrNameTag))
323+
return (key.Remove(key.Length - connStrNameTag.Length), connStrNameTag);
324+
else if (key.EndsWith(providerNameTag))
325+
return (key.Remove(key.Length - providerNameTag.Length), providerNameTag);
326+
}
327+
328+
return (key, null);
329+
}
330+
}
215331
}

0 commit comments

Comments
 (0)