增强的配置文件处理 II
可以使用配置变量和 if 逻辑来增强和扩展配置文件。
上一篇文章
引言 - 这是关于什么的
第一篇文章《增强配置文件处理》深入探讨了配置文件中可能包含变量的事实。第一篇文章隐含地假设变量是不可嵌套的,它们可能是顺序的。在本系列文章的第二也是最后一篇文章中,我们将讨论为配置文件变量添加 if 逻辑。您无需阅读第一篇文章即可理解本文,但我强烈建议您在着手阅读第二篇文章之前,务必理解第一篇文章的内容。
下一篇文章:Enhanced-Configuration3.aspx 讨论了即时评估。
背景 - 第二部分的动机
配置文件变量和现在的配置逻辑提供了配置文件的一种替代方案和便利性。第二部分,也就是本文,通过允许在配置变量中加入一些 if 逻辑并支持嵌套变量,为配置文件增加了更多灵活性。因此,例如,您可以使用一个配置文件来处理开发、UAT 和生产环境。这对配置文件和它提供的便利性来说是一个巨大的进步。
第一部分回顾
我们追踪了两种配置变量类型:“{date::format}”和“{key::value}”。其中 date 将评估为当前日期并以指定的格式显示。第二个是评估,它从同一配置文件中提取另一个键的值。我所有的示例都针对配置文件中的 <appSettings/> 部分,但我相信您能够修改本文中的信息,并将其应用于配置文件的其他部分,如 <connectionStrings/> 部分、自定义配置文件部分或映射文件(app.config 之外的另一个配置文件)。
已经可以评估
由于文章的第一部分相当详细地讨论了这些主题,我们可以评估以下(键,值)变量:
键:<add key="key1" value="{Key::keyName}"/>
日期:<add key="keyDate" value="{Date::yyyy.mm.dd}"/>
现在,我们将添加
If:<add key="keyIf" value="{if({key::key3}={Dev}){key::key5},{key::key6}}"/>
ForeignKey:<add key="key4" value="{ForeignKey::path::key-in-that-path-file}"/>
Literal:<add key="keyLiteral" value="{Literal}"/>
其中 {if ..} 表达式以及 ConfigEval 类的更改将是本文主要讨论的内容。
ForeignKey:“{ForeignKey::path::key-in-that-path-file}”评估一个(键、路径、值)三元组,其中(键、值)对存在于由“path”指定的平面文件中(分隔符是两个双冒号 {ForeignKey::path::value} 之间的部分)。路径本身可以是相对路径、绝对路径,包括 UNC 指定的路径。由 path 指向的文件包含一行行列表,每行的格式为“key=value”,其他格式的行将被忽略。如果您的需求不同,这可以为您提供一个好的起点。
最后,literal 是一个用花括号括起来的字符串常量,{this is a literal},它不是 if 表达式,也没有双冒号分隔符。我发现 literal 是有必要的,原因有两个。我们不能使用双引号,因此需要另一组分隔符,而且由于花括号已在各处使用,这是一种自然的选择了。第二个原因与重复有关:一个 if 表达式可以通过一个评估路径或另一个路径在更少的步骤中得到评估。Literal 可以使一切保持一致。例如,考虑以下键=“path dependent”的表达式:
<add key="DevPath" value="c:\temp"/>
<add key="ProdPath" value="ForeignKey::\\UNC\root\VeryImportant\SomeOtherFile.txt::"/>
<add key="DevID" value="\SomeFile.txt"/>
<add key="ProdID" value="ServerFile"/>
<add key="path dependent" value="{{Key::{key::env}Path}{key::{key::env}ID}}"/>
如果我们评估具有键=“path dependent”的配置条目,并且“env”条目设置如下:
<add key="env" value="Dev"/>
那么我们需要 5 个评估步骤才能得到一个 literal,如下所示:
<add key="path dependent" value="{{Key::{key::env}Path}{key::{key::env}ID}}"/> (1)
<add key="path dependent" value="{{key::DevPath}{key::{key::env}ID}}"/> (2)
<add key="path dependent" value="{c:\temp{key::{key::env}ID}}"/> (3)
<add key="path dependent" value="{c:\temp{key::DevID}}"/> (4)
<add key="path dependent" value="{c:\temp\SomeFile.txt}"/> (5)
我们最终得到了一个带括号的 literal:“{c:\temp\SomeFile.txt}”。因此,让一个 literal 在没有花括号分隔符的情况下评估为自身是很方便的。现在评估相同的配置条目“path dependent”,但这次我们将有:
<add key="env" value="Prod"/>
那么评估路径略有不同,需要 6 个评估步骤才能得到一个不需要 literal 评估的最终表达式:
<add key="path dependent" value="{{Key::{key::env}Path}{key::{key::env}ID}}"/> (1)
<add key="path dependent" value="{{key::ProdPath}{key::{key::env}ID}}"/> (2)
<add key="path dependent"
value="{ForeignKey::\\UNC\root\VeryImportant\SomeOtherFile.txt::{
key::{key::env}ID}}"/> (3)
<add key="path dependent"
value="{ForeignKey::\\UNC\root\VeryImportant\SomeOtherFile.txt::{key::ProdID}}"/>(4)
<add key="path dependent"
value="{ForeignKey::\\UNC\root\VeryImportant\SomeOtherFile.txt::ServerFile}"/> (5)
<add key="path dependent" value="L:\Prod\ProdFile.txt"/> (6)
因此,如果我们希望各种潜在键之间保持一致性,那么就需要允许带括号的 literal 评估为自身(不带花括号分隔符)。
解析逻辑
成功处理的关键在于能够解析我们的表达式。在上一篇文章《增强配置文件处理》中,我们使用了正则表达式模式:@"{\s*xxx\s*::(?<EvalVal>.*?)}" 来帮助我们提取“EvalVal”。这现在对我们来说已不足够。例如,考虑以下表达式:
<add key="keyname" value="{if (condition) tExpr, fExpr}"/>
对于 if 表达式,我们必须处理这样一个事实,即(条件)、tExpr 和 fExpr 本身也可能包含其他构造,例如:{key::..}、{ForeignKey::..}、{Date::..}、{if..}、{..literal..} 或您需要的任何其他自定义构造。因此,上面显示的模式 @"{\s*xxx\s*::(?<EvalVal>.*?)}" 根本不够完善,因为“.*?” 假定表达式没有嵌套。
我们需要一个新思路来处理嵌套问题。
新思路
处理这种潜在的复杂情况的新思路是能够一次“查看”一个简单的 {..} 构造。尽管我们的语法很简单,但我宁愿避免编写词法分析器和解析器,甚至简单的预测性解析器。此外,我更愿意重用我们过去成功使用的 .Net 正则表达式 Regex。
我们需要提出一个合适的模式,该模式可以提取第一个最内层或任何一个最内层的简单 {..} 构造。然后,我们可以使用我们根据第一篇文章已构建好的工具来对其进行评估。其想法是遍历表达式集合,找出最内层的“简单”构造,直到整个 {..} 构造集被解析完毕,或者我们达到最大迭代次数。(最大次数将防止循环引用。)
为了完全清楚:我所说的“简单 {..}”构造是指其内部不包含嵌套构造需要评估的构造。
考虑以下模式,它将匹配(开括号)(一些简单的东西)(闭括号),其中(一些简单的东西)允许转义的括号:
@"((?<!\\){)(?<SmplExpr>((\\{)|(\\})|([^{}]))*?)(})"
为了方便讨论,我将把匹配到的模式称为:“SmplExpr”,将带有分隔符括号的模式称为“Simple Expression”。“SmplExpr”不包含分隔符括号,“Simple Expression”包含分隔符括号。
让我们专注于我们刚才看到的这个*Simple Expression*模式;请您仔细阅读并尝试理解它。它首先是 @“((?<!\\){)” 您看到的是:一个零宽度负向后行断言,用更通俗的话来说,它只在开括号字符前面没有反斜杠时才开始匹配。结束的闭括号是第一个闭括号,这由 SmplExpr 子表达式中的星号(“*”)后面的问号(“?”)确保。因此,SmplExpr 捕获的表达式就是我们的简单构造。
在我们继续之前,让我们花点时间思考一下上面的字符串前面带有 at 符号(“@”),但它却加倍了它的反斜杠(“\”)这个事实。这是正确的!上面的字符串要经过两次字符串评估!一次通过 C# 字符串处理,一次通过正则表达式字符串处理。C# 字符串处理通过引入 at 符号(“@”)前缀使我们的工作更轻松,它不对特殊字符进行字符串处理,“所见即所得”。现在,当我们把这个完全相同的字符串通过正则表达式处理时,正则表达式评估器会将反斜杠字符(“\”)视为特殊字符。因此,如果我们要让字符串在通过正则表达式处理后保留反斜杠字符,我们就需要将其加倍。
现在我们可以继续考虑以下示例(我们之前已经见过):
<add key="path dependent" value="{{Key::{key::env}Path}{key::{key::env}ID}}"/>
那么第一个 SmplExpr 模式将产生“key::env”(不带分隔符括号)。您可以使用许多正则表达式工具和测试器自行测试。其中一个工具可在 http://regexlib.com/RETester.aspx 上找到。
为了说明重点:如果我们不断地重复使用 Simple Expression 构造,我们将最终评估整个字符串,或者达到最大迭代次数(当我们断定定义有循环引用时)。
这个绝妙想法的局限性
考虑以下(键,值)对:
<add key="k1" value="="/>
<add key="k2" value="="/>
<add key="k3" value="{if ({key::k1} = {key::k2}) Life’s GREAT, Life’s good}"/>
其中“k3”将不会按预期进行评估。
另一个局限是我们无法将逗号包含在 tExp 或 fExp 中。(为什么?)
由于我们允许将转义的花括号视为普通字符,我们可以(确实可以)将花括号包含在任何表达式中。
克服局限
有几种方法可以解决刚才指出的局限性:
- 忍受它。这些限制并非如此之大,了解它们可以让我们提前规划。此外,有时解决不必要的问题只会带来更多头痛和副作用。在本文中,我们将这样做——忍受它。
- 修改匹配表达式模式以包含转义的逗号和转义的等号,或者至少在“if”匹配表达式模式中包含。我不喜欢这个解决方案,因为它具有最多的副作用和特殊情况。
- 重写 ProcessXXX 类和 ConfigEval 以创建解析树。我认为这是技术上最健全的解决方案,而且工作量很大,但它带来的好处不足以证明所花费的精力是合理的。此外,当需要别人(或您自己)添加自定义 ProcessXXX 类时,您需要付出类似的努力来创建它。出于上述原因,我将不深入研究创建一组生成解析树的类。
谁负责什么
我们期望每个构造处理程序都能够评估它所知的 Simple Expression 模式。这意味着:
- ProcessKey(..) {Key::value} 的处理程序将能够识别 {key::xxx} 简单模式并能够对其进行评估。
- ProcessDate(..) {Date::format} 的处理程序将识别并评估 {Date::xxx}
- ProcessForeignKey(..) {ForeignKey::path::value} 的处理程序将识别并评估外键。
- ProcessIf(..) {if (condition) tExp, fExp} 的处理程序将识别并评估所有可能的“if”简单表达式。
- ProcessLiteral(..) 字面量表达式的处理程序将识别并评估字面量。
最后,如果一个表达式未被识别,我们期望系统会像对待普通文本一样处理它,不做任何操作,然后继续。
If 的冲刺阶段
那么,让我们精确地定义“{if (condition) tExp, fExp}”
“if”构造必须以字面量“if”开头,后跟一个布尔值括号表达式,后跟一个表达式(真表达式),后跟一个逗号,最后以一个表达式(假表达式)结尾。
条件
“{if (condition) tExp, fExp}”:它是一个产生真或假值的构造。它可以是以下之一:
- DirectoryExists({directoryName})
- FileExists({fileName})
- {Expression1} = {Expression2}
评估 IF
我们从一个识别“简单 if”构造的正则表达式开始。它可能是我们上面定义的三个形式之一:
- @"((?<!\\){)\s*if\s*
\(\s*DirectoryExists\s*\(\s*(?<directoryName>((\\{)|(\\})|([^{}]))*?)\s*\)\s*
(?<texp>((\\{)|(\\})|[^{}])*?)\s*,\s*(?<fexp>((\\{)|(\\})|[^{}])*?) - @"((?<!\\){)\s*if\s*
\(\s*FileExists\s*\(\s*(?<fileName>((\\{)|(\\})|([^{}]))*?)\s*\)\s*
(?<texp>((\\{)|(\\})|[^{}])*?)\s*,\s*(?<fexp>((\\{)|(\\})|[^{}])*?) - @"((?<!\\){)\s*if\s*
\(\s*(?<exp1>(\\{)|(\\})|([^{}]))*?)\s*=\s*
(?<exp2>(\\{)|(\\})|([^{}]))*?)\s*\)
(?<texp>((\\{)|(\\})|[^{}])*?)\s*,\s*(?<fexp>((\\{)|(\\})|[^{}])*?)
让您的眼睛吸收花括号的爱,即使它在您的大脑深处感到痛苦并刺痛您的眼睛,也要记住它能塑造您的性格,让您(如果您是男性)更有男子气概,并且在阅读花括号时,不要让您的身体扭曲成意大利面条形状!!!
您以前见过这种模式,现在唯一复杂的因素是转义的花括号。以前,很久以前,在我们允许转义的花括号被视为普通字符之前,我们遇到了像:“.*?” 这样的模式,一个“婴儿”模式。记住:@"{\s*xxx\s*::(?<EvalVal>.*?)}”。我们不能继续使用婴儿模式,因为它无法处理嵌套模式。现在,当我们想允许简单表达式模式时,我们的 @"{\s*xxx\s*::(?<EvalVal>.*?)}" 将变成 @"(?<!\\){\s*xxx\s*::(?<EvalVal>([^{}])*?)}”。最后,我们允许转义的花括号,我们的“婴儿模式”,“.*?” 长成了“成人”模式:“((\\{)|(\\})|[^{}])*?”。我们甚至给我们的成人模式起了个昵称:“simple expression”。上面花括号中的其余模式都是老套路了。
如果您一两次地查看“成人模式”,然后思考三到四次,再分析五到六次,您就会立刻注意到,如果该模式后面紧跟着一个闭括号,例如:@"((?<!\\){)\s*Key\s*::(?<EvalVal>((\\{)|(\\})|([^{}]))*?)(})",那么我们可以省略一个闭括号,如下所示:@"((?<!\\){)\s*Key\s*::(?<EvalVal>((\\{)|(\\})|([^{]))*?)(})”。这太微不足道了,我觉得关注这种优化很容易出错,因为并非所有模式都以闭括号结尾(请参阅 if 表达式,其中模式不以闭括号(“}”)结尾)。
Using the Code
让我们回顾一下,弄清楚如何将所有这些东西组合在一起,并最终看一些代码。我们需要能够处理以下类型的表达式:
- Date::format
- ForeignKey::full path or UNC path or relative path::value
- If (condition) tExpr, fExpr
- Key::value
- Literal - {just a string that has no “::” nor “if” in the beginning of the string}
- Unknown and cannot be evaluated
我们还意识到,我们需要将这些模式应用于配置文件中具有 {..} 表达式的每个条目。
不许触碰我们的隐私
与第一部分不同,在第一部分中我们将 ConfigEval 类的私有 _configEntry 暴露给了 ProcessKey 类,这次我们将通过引入一个新类—ConfigElement 来进行收紧。
public sealed class ConfigElement
{
public ConfigElement(string key, string value)
{
Key = key;
Value = value;
}
public string Key { get; set; }
public string Value { get; set; }
}
非常简单!
此外,让我们在 ConfigEval 类中引入几个处理触碰的函数:GetConfigElement() 和 Attach()。在评估 {key::value} 配置变量时,我们需要访问 _configEntry 集合的成员元素。我们将使用 GetConfigElement() 方法来实现这一点。在处理 app.config 文件时,使用我们的 Config 类,我们将使用 Attach() 成员函数来构建 _configEntry 集合。
protected virtual ConfigElement GetConfigElement(string key)
{
if (!_configEntry.ContainsKey(key))
return null;
return _configEntry[key];
}
...
...
protected virtual void Attach(string key, string value)
{
ConfigElement element = new ConfigElement(key, value);
if (_configEntry.ContainsKey(key))
ReportError(string.Format("Item with key '{0}' was already added", key));
else
_configEntry.Add(key, element);
}
括号计数安全
我们的算法和模式都隐式地假定我们的括号是平衡的。我们的模式匹配只匹配平衡的括号。因此,如果我们想将不平衡的括号标记为错误,我们需要一个单独的机制。幸运的是,正则表达式的工具库正好有这样的分组构造:“(?<close-open> subexpression)”,并且该分组命名为“Balancing group definition”(平衡组定义)。这个分组构造删除了先前定义的组“open”的定义,并将匹配的定义存储在组“close”中,这是先前定义的“open”组和当前组之间的间隔。
因此,我们检查开/闭括号如下:
@"^((\\{)|(\\})|([^{}]))*(((?<Open>(?<!\\){)((\\{)|(\\})|([^{}]))*)+((?<Close-Open>(?<!\\)})((\\{)|(\\})|([^{}]))*)+)*(?(Open)(?!))$"
“它从字符串的开头开始,请注意脱字符(“^”)符号,并在字符串的末尾结束,请注意末尾的美元符号(“$”)符号。脱字符后面是我们的成人模式:“(\\{)|(\\})|([^{}]))*”匹配任何不是括号的字符,但允许转义的括号,出现 0 次或多次。接下来:“((?<Open>(?<!\\){)”定义了一个名为“Open”的组,一个未转义的开括号。然后是我们的成人模式“((\\{)|(\\})|([^{}]))*”,匹配所有非括号字符。对所有连续的开括号执行此操作(简化版)“(?<Open>{)--all non brace--)+”,然后将下一个闭括号与最内层的开括号匹配,并移除每个“Open”组,如果有“close”,则执行此操作:“(?<Close-Open>(?<!\\)})”。所有开闭括号的匹配(简化版)捕获在:“(((?<Open>{)--all non brace--)+((?<Close-Open>})--all non brace--)+)*”。最后,根据是否有剩余/缺失的“Open”组,返回匹配或不匹配,如下所示:“(?(Open)(?!))”。
公众熟悉且已知
我将不回顾 ProcessXXX() 系列例程:ProcessDate()、ProcessForeignKey()、ProcessIf()、ProcessKey() 和 ProcessLiteral(),因为它们与第一篇文章中解释的处理过程非常相似。请阅读它们,如果您有任何问题,请使用文章末尾的消息给我写信,我将回复。增加的复杂性是使用其成人对应项:“((\\{)|(\\})|[^{}])*?”替换了婴儿模式“.*?”。第二个增强是为 ProcessXXX() 例程系列添加了一个接口。
public interface IProcessEvaluate
{
bool Evaluate(ConfigElement configElem);
}
与上一部分一样,类的层次结构是:ConfigEval 是 Config 的基类。
ConfigEval
ConfigEval 是我们评估的核心。它有一个静态构造函数(类构造函数—“cctor”),我们在其中定义了 Simple Expression 模式。它还有一个构造函数(非静态构造函数—“ctor”),它使用各种“Evaluate()”函数填充委托。
_evaluateHandler = (new ProcessDate()).Evaluate;
_evaluateHandler += (new ProcessKey(GetConfigElement)).Evaluate;
_evaluateHandler += (new ProcessForeignKey()).Evaluate;
_evaluateHandler += (new ProcessIf()).Evaluate;
_evaluateHandler += (new ProcessLiteral()).Evaluate;
现在让我们看一下 Evaluate()
函数的签名:
public bool Evaluate(ConfigElement configElem)
那些熟悉旧版 .Net 的人会立即注意到 Evaluate() 函数返回一个值,因此不能作为多播委托的一部分。此外,即使 Evaluate() 可以成为多播委托的一部分,返回值也会丢失,而我们需要它,这是我们用来知道是否发生了评估的机制。
我们有一个解决所有这些混乱的方法:.Net 2.0 引入了 GetInvocationList() 函数,它解决了我们所有的问题(它解决了我们的财务问题、婚姻问题、社交问题……几乎所有问题。)
除了使用多播委托和 GetInvocationList() 函数之外,还有另一种做事方式。我们可以按顺序调用每个 ProcessXXX 类的 Evaluate() 函数。我更喜欢多播委托,因为它在类的开头(构造函数)提供了一个位置来添加所有 Evaluate() 函数,然后使用 for 循环来遍历 Evaluate() 函数。我认为当您需要添加自己的 ProcessXXX 类时,这会减少出错的可能性。尽管一种方法(多播委托)与另一种方法(按顺序调用 Evaluate() 函数)的优势很小,因为我们需要访问 Evaluate() 函数的唯一地方是 EvaluateConfig() 函数。
EvaluateConfig
private const int PassThroughUpperLimit = 10000; // Prevent infinite looping
protected virtual void EvaluateConfig()
{
_evalSuccess = true;
//
// Retrieve all nodes needing attention, into a linked list.
// For the initialization: "needing attention" will mean a node that has
// a simple expression in it.
//
LinkedList<ConfigElement> configElements = new LinkedList<ConfigElement>();
IEnumerable<ConfigElement> configNodes =
from k in _configEntry where IsSimpleExpression(k.Value.Value) select k.Value;
foreach (ConfigElement configNode in configNodes)
configElements.AddLast(configNode);
if (configElements.Count == 0)
return;
for (int i = 0; i < PassThroughUpperLimit; ++i)
{
// Cycle through the linked list
for (LinkedListNode<ConfigElement> configNode = configElements.First;
configNode != null; )
{
// Guard against an expression that has no ProcessXXX to take care of.
bool bEval = false; // Did we evaluate anything, initialize to false.
bool rc = IsSimpleExpression(configNode.Value.Value);
if (rc)
{
// Cycle through all the ProcessXXX that we have attached to this
// _evaluateHandler delegate. If we are to do away with this delegate
// in favor of calling the ProcessXXX.Evalute(..) separately then
// this is where it will take place.
foreach (Delegate del in _evaluateHandler.GetInvocationList())
{
rc = (bool)del.DynamicInvoke(new object[] { configNode.Value });
if (rc)
{
// Yes we did evaluate something.
bEval = true;
// Now move on to the next config entry that needs evaluation.
// Moving on to the next config entry may not be your choice.
// I feel that this is the quickest way to evaluate the entire
// config file given that the keys may refer to a sequentially
// previous as well as a sequentially next occurring entry.
break;
}
}
}
if (!bEval)
{
// Remove node -- there is nothing to evaluate any longer
LinkedListNode<ConfigElement> p = configNode.Next;
configElements.Remove(configNode);
configNode = p;
}
else
configNode = configNode.Next;
}
}
if (configElements.Count > 0)
{
ReportError(
"Too many iterations through the config file. Check circular referencing");
_evalSuccess = false;
}
}
首先,该函数构建一个包含所有需要评估的配置条目的链接列表。请看表达式:
// Retrieve all nodes needing attention, into a linked list
LinkedList<ConfigElement> configElements = new LinkedList<ConfigElement>();
IEnumerable<ConfigElement> configNodes =
from k in _configEntry where IsSimpleExpression(k.Value.Value) select k.Value;
foreach (ConfigElement configNode in configNodes)
configElements.AddLast(configNode);
让我们看一下 LINQ 表达式,即 configNodes
的集合部分:
from k in _configEntry
where IsSimpleExpression(k.Value.Value)
select k.Value
该表达式将从 _configEntry
中检索 IsSimpleExpression(..)
为 true
的值,这正是我们所寻找的。
现在,对于我们的评估,我们有一个双重循环结构。外层循环监视失控的无限迭代过程,第二层(内层循环)评估链接列表中每个节点的第一个简单表达式。内层循环是魔法发生的地方:
// Guard against an expression that has no ProcessXXX to take care of with bEval.
bool bEval = false; // Did we evaluate anything, initialize to false.
bool rc = IsSimpleExpression(configNode.Value.Value);
if (rc)
{
// Cycle through all the ProcessXXX that we have attached to this
// _evaluateHandler delegate. If we are to do away with this delegate
// in favor of calling the ProcessXXX.Evalute(..) separately then
// this is where it will take place.
foreach (Delegate del in _evaluateHandler.GetInvocationList())
{
rc = (bool)del.DynamicInvoke(new object[] { configNode.Value });
if (rc)
{
// Yes we did evaluate something.
bEval = true;
// Now move on to the next config entry that needs evaluation.
// Moving on to the next config entry may not be your choice.
// I feel that this is the quickest way to evaluate the entire
// config file given that the keys may refer to a sequentially
// previous as well as a sequentially next occurring entry.
break;
}
}
}
if (!bEval)
{
// Remove node -- there is nothing to evaluate any longer
LinkedListNode<ConfigElement> p = configNode.Next;
configElements.Remove(configNode);
configNode = p;
}
else
configNode = configNode.Next;
代码首先通过调用 IsSimpleExpression(..)
函数来询问配置条目中是否存在简单表达式。如果存在简单表达式,那么我们将依次调用委托的 Evaluate()
函数(针对每个 ProcessXXX 类),使用 GetInvocationList()
构造。del.DynamicInvoke(..)
将按照构造函数中给出的顺序调用 Evaluate(..)
函数,并在评估后移至下一个配置条目(下一个节点)。
这种情况将保持未识别的配置条目不变,然后继续。
EvaluateConfig()
是 ConfigEval
类的工作马。我还有两个相关的其他方法:一个名为 PreEvaluateConfig()
,另一个名为 PostEvaluateConfig()
。Config 类的构造函数最后调用 PreEvaluateConfig()
,然后调用 EvaluteConfig()
,最后调用 PostEvaluateConfig()
。PreEvaluateConfig()
是我通过调用 IsBalancedCurlyBraces()
放置平衡括号检查的地方。PostEvaluateConfig()
是我移除转义的括号字符序列中的转义(“\”)字符的地方。
收尾
现在看起来我们完成了,我想处理一个非常罕见的情况。如果,天哪,您需要在最终配置条目中有一个反斜杠放在括号前面。在这种情况下,您需要在配置文件中加倍反斜杠,这样在所有需要删除的内容被删除后,就会留下一个反斜杠。
<add key="Heaven Help Us" value="\\{"/>
这样,您就会得到一个开括号,它不会被 PreEvaluateConfig() 标记为错误。
接下来我们去哪里
您可能希望添加一个(键、值)对,它保持表达式不变,不尝试评估。例如,您可以添加一个“LeaveBe”键(在 {key::value} 对中):<add key="As Is" value="{LeaveBe::you may include unbalanced curlies, commas and heaven knows what else, without the worry of condemnation}"/>。
如果您需要进一步处理怎么办?
例如,您需要进一步处理来解密连接字符串。所有这些,就像过去一样,都包含在 Config 类属性中,该属性检索连接字符串。请记住,我们有构造函数使用 Attach(..) 方法构建了 _configEntry。然后 _configEntry 中的每个条目都有一个属性,该属性实际暴露该条目,并且只评估一次,Config.ConnectionString 属性。您可以在这里解密连接字符串或对条目执行其他处理。
是时候做选择了
现在,您可以在本期文章和上一期文章之间进行选择,如第一篇文章《增强配置文件处理》中所述。问题是,您将为您的项目选择哪一个?第一部分更简单易于实现和维护。因此,如果您不预期需要进行嵌套评估,那么选择就很清楚了,第一部分。另一方面,如果需要嵌套评估,例如需要 if 表达式,那么第二部分(本期)是您的明确选择。如果您觉得本期文章很合适,那么即使您不需要嵌套功能,您仍然可以选择第二部分,因为它提供了灵活性。或者仅仅是因为这是您为自己设定的标准。此外,即使您现在只需要少量功能,您也为将来的扩展留有余地。
结语
使用 KISS 原则(保持简短和简单)。我们现在拥有一个功能强大且可扩展的系统,它非常具有表现力。这是能力的双刃剑。一方面,它极具表现力——这是好的一面。另一方面,您现在可以创建非常复杂的表达式,这些表达式很难理解——这是黑暗的一面。在复杂和富有表现力之间,以及简短和简单之间,请运用您的判断力。使用“if”构造和“ForeignKey”构造,您现在可以拥有一个在开发、UAT 和生产环境之间无需更改的单个配置文件;简单、漂亮且易于理解。
祝您愉快,
Avi
历史
- 2009/3/19 首次提交
- 2009/9/12 公布文章 III (增强配置文件处理 III)