Use second capture from tokenPattern to supply potential default values in Token mode. (#247)

This commit is contained in:
Steve Molloy 2024-10-08 20:49:27 -07:00 коммит произвёл GitHub
Родитель c301f46784
Коммит baf4aa0192
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
3 изменённых файлов: 26 добавлений и 6 удалений

Просмотреть файл

@ -59,10 +59,22 @@ This is a setting that defines which characters from a config key need to be rep
Somewhat of a remnant from earlier versions that allowed operating on raw XML, this flag can be used to specify whether or not to XML-escape values when replacing tokens in 'Token' mode. The default value is `false`.
#### tokenPattern
This is a setting that is shared between all KeyValueConfigBuilder-derived builders is `tokenPattern`. When describing the `Expand` behavior of these builders 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. 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 in their token names. Additionally there might be scenarios where the `${}` pattern is not acceptable.
A setting that is shared between all KeyValueConfigBuilder-derived builders is `tokenPattern`. When builders are in `Token` mode, [section handlers](SectionHandlers.md) are used to iterate through the Key/Value pairs of a config section. The builder then searches the full text of each 'Value' for any 'tokens' to expand. (In the old `Expand` mode, this search was done on the entire raw xml definition of the section before the .Net config system turns that into an object.) This token search is done with a regular expression, and the format of the token is given by this `tokenPattern` setting. The current default pattern is `@"\$\{(\w[\w-_$@#+,.:~]*)\}"`, which is the familiar "$\{TOKEN}" pattern we frequently see in other places.
`tokenPattern` allows developers to change the regex that is used for token matching. It is a string argument, and no validation is done to make sure it is 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, and the first capture must be the token name to look up in the config source.
> :information_source: **NOTE:**
> In v3.1, pattern recognition has been updated to look for a second capture within the `tokenPattern` match. If one is found, then that second capture will be used as a default value if no value can be found in the backing configuration store for the builder. The out-of-box `tokenPattern` is still as it was in the previous version, not capturing default values. But v3.1 builders will light up with this new feature simply by updating the `tokenPattern` in builder definitions like so:
> ```xml
> <configBuilders>
> <builders>
> <!-- This will find tokens like "${TOKEN}" and "${TOKEN:default}" -->
> <add name="DefaultEnv" mode="Token" tokenPattern="\$\{(\w[\w-_$@#+,.~]*)(?:\:([^}]*))?\}" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment" />
> </builders>
> </configBuilders>
> ```
> The [SampleConsoleApp](../samples/SampleConsoleApp/App.config) has been updated to demonstrate this new feature.
## AppSettings Parameters
Starting with version 2, initialization parameters for key/value config builders can be drawn from `appSettings` instead of being hard-coded in the config file. This should allow for greater flexibility when deploying solutions with config builders where connection strings need to be kept secure, or deployment environments are swappable. Eg:

Просмотреть файл

@ -18,6 +18,7 @@
<add name="Json" mode="Greedy" jsonMode="Flat" jsonFile="${jsonFile}" type="Microsoft.Configuration.ConfigurationBuilders.SimpleJsonConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Json" />
<add name="JsonCS" mode="Strict" jsonMode="Sectional" jsonFile="${jsonFile}" type="Microsoft.Configuration.ConfigurationBuilders.SimpleJsonConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Json" />
<add name="JsonExpand" mode="Token" jsonMode="Sectional" jsonFile="${jsonFile}" type="SamplesLib.ExpandWrapper`1[[Microsoft.Configuration.ConfigurationBuilders.SimpleJsonConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Json]], SamplesLib" />
<add name="DefaultEnv" mode="Token" tokenPattern="\$\{(\w[\w-_$@#+,.~]*)(?:\:([^}]*))?\}" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment" />
</builders>
</configBuilders>
@ -29,9 +30,13 @@
</handlers>
</Microsoft.Configuration.ConfigurationBuilders.SectionHandlers>
<appSettings configBuilders="Env,KeyPerFile">
<appSettings configBuilders="Env,KeyPerFile,DefaultEnv">
<add key="SampleItems" value="~/../../../SampleItems" />
<add key="jsonFile" value="~/../../../SampleWebApp/App_Data/settings.json" />
<add key="TestingEnvDefaults1" value="${WINDIR:Should not see default.}" />
<add key="TestingEnvDefaults2" value="${ENV_VAR_DOES_NOT_EXIST:But a default value is here.}" />
<add key="TestingEnvDefaults3" value="${EMPTY_DEFAULT_VALUE:}" />
<add key="TestingEnvDefaults-DefaultValueNotRequiredWithThisPattern" value="${OS}" />
</appSettings>
<connectionStrings configBuilders="JsonCS">

Просмотреть файл

@ -78,6 +78,7 @@ namespace Microsoft.Configuration.ConfigurationBuilders
public string TokenPattern { get { EnsureInitialized(); return _tokenPattern; } protected set { _tokenPattern = value; } }
//private string _tokenPattern = @"\$\{(\w+)\}";
private string _tokenPattern = @"\$\{(\w[\w-_$@#+,.:~]*)\}"; // Updated to be more reasonable for V2
//private string _tokenPattern = @"\$\{(\w[\w-_$@#+,.~]*)(?:\:([^}]*))?\}"; // Something like this to allow default values
/// <summary>
/// Gets or sets the behavior to use when recursion is detected.
@ -207,19 +208,20 @@ namespace Microsoft.Configuration.ConfigurationBuilders
configValue = Regex.Replace(configValue, _tokenPattern, (m) =>
{
string settingName = m.Groups[1].Value;
string defaultValue = (m.Groups[2].Success) ? m.Groups[2].Value : m.Groups[0].Value;
// If we are processing appSettings in ProcessConfigurationSection(), then we can use that. Other config builders in
// the chain before us have already finished, so this is a relatively consistent and logical state to draw from.
if (CurrentSection is AppSettingsSection appSettings && CurrentSection.SectionInformation?.SectionName == "appSettings")
return (appSettings.Settings[settingName]?.Value ?? m.Groups[0].Value);
return (appSettings.Settings[settingName]?.Value ?? defaultValue);
// Try to use CurrentConfiguration before falling back to ConfigurationManager. Otherwise OpenConfiguration()
// scenarios won't work because we're looking in the wrong processes AppSettings.
else if (CurrentSection?.CurrentConfiguration?.AppSettings is AppSettingsSection currentAppSettings)
return (currentAppSettings.Settings[settingName]?.Value ?? m.Groups[0].Value);
return (currentAppSettings.Settings[settingName]?.Value ?? defaultValue);
// All other config sections can just go through ConfigurationManager to get app settings though. :)
return (ConfigurationManager.AppSettings[settingName] ?? m.Groups[0].Value);
return (ConfigurationManager.AppSettings[settingName] ?? defaultValue);
});
_config[configName] = configValue;
@ -362,10 +364,11 @@ namespace Microsoft.Configuration.ConfigurationBuilders
string updatedString = Regex.Replace(rawString, TokenPattern, (m) =>
{
string key = m.Groups[1].Value;
string defaultValue = (m.Groups[2].Success) ? m.Groups[2].Value : m.Groups[0].Value;
// Same prefix-handling rules apply in token mode as in strict mode.
// Since the key is being completely replaced by the value, we don't need to call UpdateKey().
return EscapeValue(GetValueInternal(key)) ?? m.Groups[0].Value;
return EscapeValue(GetValueInternal(key)) ?? defaultValue;
});
return updatedString;