Succeeding Article
Enhanced String Handling
Introduction - What is It All About
The first article, Enhanced Configuration File Handling, delves into the fact that configuration files may have variables. The first article has an implicit assumption that the variables are not nested, they may be sequential. In this second and last installment of the article we will discuss adding if-logic to the config-variables. You need not read the first article to understand this installment, though I highly recommend that you do understand the first article before you tackle this second one.
Next article: Enhanced-Configuration3.aspx discusses evaluation on-the-fly.
Background - Motivation for the Second Installment
The configuration variables and now configuration logic come to provide an alternative and convenience to config files. The second installment, this work, adds more flexibility to the configuration file by allowing for some if-logic within the config-variables and allows for nested variables. So now, for example, one can have a single configuration file for the Dev, UAT and Prod environments. This is a big win for the config files and the convenience that it affords.
Recap of First Installment
We tracked two configuration variable types: “{date::format}” and “{key::value}”. Where the date will evaluate to the current date and present it in the format specified. The second is an evaluation yielding another key’s value from within the same config file. All my examples target the <appSettings/> section the config file, however, I trust that you are able to modify the information herein and apply it to other sections of the config file, like the <connectionStrings/> section, a custom section of a config file or a map file (another config file other than the one originating from app.config).
Already can Evaluate
As the first installment of the articles discussed these topics, fairly thoroughly, we can evaluate the following (key, value) variables:
Key: <add key="key1" value="{Key::keyName}"/>
Date: <add key="keyDate" value="{Date::yyyy.mm.dd}"/>
Now, we will add:
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}"/>
Where {if ..} expression and the change to the ConfigEval class is what this article will concentrate on, for the most part.
ForeignKey: “{ForeignKey::path::key-in-that-path-file}” evaluates a (key, path, value) triplet, where the pair (key, value) lives in a flat file specified by the “path” (the construct between the two double colons {ForeignKey::path::value}). The path itself may be a relative path or an absolute path including a UNC specified path. The file, pointed to by path, contains a list of lines, each one of which is of the format “key=value”, any other formatted line is ignored. This is a good starting point for you if your needs are different.
Lastly, literal is a string constant surrounded by braces, {this is a literal}, that is not an if-expression nor does it have a double colon separator. I found literals to be necessary for two reasons. We cannot use double quotes therefore we needed another set of delimiters and since braces are being used everywhere else it was a natural choice. The second reason has to do with doubling up: an if-expression may be evaluated in fewer steps via one path of evaluation or another. A literal makes it possible to keep everything consistent. For example consider the following expression in key=“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}}"/>
If we evaluate config entry having its key=“path dependent” and also having the “env” entry set like so:
<add key="env" value="Dev"/>
Then we need 5 steps of evaluation to reach a literal, like so:
<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)
We ended up on a bracketed literal: “{c:\temp\SomeFile.txt}”. So having a literal evaluate to itself without the brace delimiters is comfortable. Now evaluate the same config entry “path dependent” but this time we will have:
<add key="env" value="Prod"/>
Then the path of evaluation is a bit different and it takes 6 steps of evaluation to reach a final expression that requires no literal evaluation:
<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)
So if we want to be able to have consistency between the various potential keys then we need to allow for bracketed literals to be evaluated to themselves (no brace delimiters).
Parsing Logic
The key to a successful processing is to be able to parse our expressions. In the previous installment of the article, Enhanced Configuration File Handling, we employed the regular expression pattern: @"{\s*xxx\s*::(?<EvalVal>.*?)}" in order to help us with extracting the “EvalVal”. This is not good enough for us now. Consider, for example, the following expression:
<add key="keyname" value="{if (condition) tExpr, fExpr}"/>
With the if-expression we have to contend with the fact that the (condition), in the above expression, as well as tExpr and fExpr may all have other constructs as well like: {key::..}, {ForeignKey::..}, {Date::..}, {if..}, {..literal..} or whatever other custom constructs you needed to employ. So the pattern shown above @"{\s*xxx\s*::(?<EvalVal>.*?)}", is simply not sophisticated enough, because the “.*?” assumes no nesting of expressions.
We need a fresh idea to deal with the nesting business.
A Fresh Idea
The fresh idea for processing this potential monstrosity is to be able to “look” at one simple {..} construct at a time. Even though our grammar is simple I would have preferred to stay away from writing a lexical analyzer and a parser, not even a simple predictive parser. Furthermore, I will prefer to reuse the .Net regular expression, Regex, that we employed successfully in the past.
We need to come up with an appropriate pattern that plucks out the first innermost, or an innermost, simple {..} construct. Then we can evaluate it using the tools we already built as per article I. The idea is to iterate through the set of expressions and ferret out innermost “simple” constructs, until the whole set of {..} constructs is resolved or we have reached an upper limit of iterations. (The upper limit will prevent circular referencing.)
And just to be absolutely clear: by “simple {..}” construct I mean a construct that does not have nested constructs to evaluate within it.
Consider the following pattern that will match on (open brace) (something-something-simple) (close brace) whereas within the (something-something-simple) we allow escaped braces:
@"((?<!\\){)(?<SmplExpr>((\\{)|(\\})|([^{}]))*?)(})"
For the sake of discussion I will refer to the matched pattern by the name of: “SmplExpr” and to the pattern with the delimiting braces as “Simple Expression”. “SmplExpr” does not contain the delimiting braces and “Simple Expression” does contain the delimiting braces.
Let’s concentrate on this Simple Expression pattern that we just saw; let your eyes scan it and see if you can make sense of it. The first thing in it is @“((?<!\\){)” what you see is: a zero-width negative lookbehind assertion and in more normal English you will start the match on an open brace character only if it is not preceded by a backslash. The ending close brace is the first closing brace as ensured by the question mark (“?”) following the asterisk (“*”) in the SmplExpr subexpression. So the expression captured by SmplExpr is our Simple construct.
Before we move on let’s take a moment and ponder the fact that the string above is prefixed with the at sign (“@”), yet it doubles its back-slash characters (“\”). This is correct! The string above is going through string evaluation twice!!! Once through the C# string processing and once through the regular expression string processing. The C# string processing made life easier for us by introducing the at sign (“@”) prefix which does no string processing for special characters, what you see is what you get type of a deal. Now, when we pass that very same string through the regular expression wringer, then the regular expression evaluator treats the back slash character (“\”) as special. Therefore, if we are to have our string retain a back-slash character after it is through the regular expression wringer then we need to double it.
Now we can move on to consider the following example (that we saw before):
<add key="path dependent" value="{{Key::{key::env}Path}{key::{key::env}ID}}"/>
Then the first SmplExpr pattern yields “key::env” (no delimiting braces). You may test this yourself with using many of the regular expression tools and testers. One such tool is provided in http://regexlib.com/RETester.aspx.
To drive the point home: if we keep on employing the Simple Expression construct repeatedly we will ultimately evaluate the entire string or hit an upper limit of iterations (when we conclude that the definition has a circular reference).
Limitations of this great idea
Consider the following (key, value) pairs:
<add key="k1" value="="/>
<add key="k2" value="="/>
<add key="k3" value="{if ({key::k1} = {key::k2}) Life’s GREAT, Life’s good}"/>
Where “k3” will not be evaluated as expected.
Another limitation is our inability to include a coma as part of the tExp or fExp. (Why?)
We can (yes can) include braces as part of any expression because we allow for an escaped brace to be treated like any other plain, ordinary, run of the mill character.
Overcoming limitations
There are a few ways to deal with the limitations just pointed out:
- Live with it. These are not such big limitations and knowing about them allows us to plan accordingly. Moreover, at times solving things that need not be solved only create more headaches and side effects. In this article we will do just that—live with it.
- Alter the matching expression pattern to include escaped commas and escaped equal signs, or at least within the “if” matching expression pattern. I do not like this solution as it is the one with most side effects and special conditions.
- Rewrite the ProcessXXX classes and ConfigEval to create a parse tree. I feel that this is most technically sound solution and it is a lot of work though the advantage it gives us does not justify the effort spent. Moreover, when time comes for someone else, or yourself, to add a custom ProcessXXX class you will need to put similar efforts to create it. For these aforementioned reasons I will not delve into creating a set of classes that produce a parse tree.
Who handles what
We expect each construct handler to be able to evaluate the Simple Expression pattern that it knows of. This means that:
- ProcessKey(..) the handler for the {Key::value} will be able to recognize the {key::xxx} simple pattern and be able to evaluate it.
- ProcessDate(..) the handler for {Date::format} will recognize and evaluate {Date::xxx}
- ProcessForeignKey(..) the handler for {ForeignKey::path::value} will recognize and evaluate a foreign key.
- ProcessIf(..) the handler for {if (condition) tExp, fExp} will recognize and evaluate all possible “if” simple expressions.
- ProcessLiteral(..) handler for a literal expression, will recognize and evaluate a literal
Lastly, if an expression is not recognized we expect that the system will treat it as such, by doing nothing to it and move on.
The If home stretch
So let’s define the “{if (condition) tExp, fExp}” precisely:
The “if” construct must start with the literal “if” followed by a Boolean parenthetical expression, followed by an expression (the true expression), followed by a comma and ending with an expression (the false expression).
Condition
“{if (condition) tExp, fExp}”: It is a construct that yields true or false. It may be one of the following:
- DirectoryExists({directoryName})
- FileExists({fileName})
- {Expression1} = {Expression2}
Evaluating the IF
We start with a regular expression identifying the “simple if” construct. It may be one of three forms that we just defined above:
- @"((?<!\\){)\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*\)\s*
(?<texp>((\\{)|(\\})|[^{}])*?)\s*,\s*(?<fexp>((\\{)|(\\})|[^{}])*?)}"
Let your eyes soak in the love of the squiggles, even if it hurts deep down in the brain and pokes you in the eyes remember that it builds character and puts hair on your chest (only if you belong to the male species) and while you read the squiggles don’t let your body contort into a pretzel shape figure!!!
You have seen this kind of patterns before, the only complication thrown into the mix now is the escaped braces. Before hand, a long time ago, before we allowed escaped braces to be treated as a regular, ordinary, run of the mill characters, we encountered a pattern like: “.*?”, a baby pattern. Remember: @"{\s*xxx\s*::(?<EvalVal>.*?)}". We could not keep on employing the baby pattern since it could not deal with nested patterns. Now that we would like to allow simple expression pattern so our @"{\s*xxx\s*::(?<EvalVal>.*?)}" will become @"(?<!\\){\s*xxx\s*::(?<EvalVal>([^{}])*?)}". Finally we allow escaped braces in, our baby pattern, “.*?” grew up into an adult: @“((\\{)|(\\})|[^{}])*?”. We even gave our adult pattern a pet name: “simple expression”. The rest of the patterns in the above squiggles is just same old, same old.
If you look at the “adult pattern” once or twice and think about it three to four times then analyze it five or six times, you will instantly notice that if the pattern is succeeded with a close brace for example: @"((?<!\\){)\s*Key\s*::(?<EvalVal>((\\{)|(\\})|([^{}]))*?)(})" then we can save on one close-brace as such: @"((?<!\\){)\s*Key\s*::(?<EvalVal>((\\{)|(\\})|([^{]))*?)(})". This too minor and I feel that paying attention to this optimization is error prone because not all patterns end with a close brace (see the if expression for patterns that do not end with a close-brace (“}”)).
Using the Code
Let’s recap and figure out how to put the whole kit and caboodle together and finally look at some code. We need to be able to deal with the following type of expressions:
- 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
We also realize that we need to run these patterns against every entry in the config file that has an {..} expression.
Let no one touch our privates
Unlike the first installment where we exposed our private _configEntry (of the ConfigEval class) to the ProcessKey class, this time we will tighten this up by introducing a new class—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; }
}
As simple as can be!
Also let’s introduce a couple of functions in the ConfigEval class that handle touching, GetConfigElement() and Attach(). During the evaluation of the {key::value} config-variable we will need to access a member element of the _configEntry collection. We will do so with the aid of the GetConfigElement() method. During the processing of the app.config file, using our Config class, we will build the _configEntry collection using the Attach() member function.
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);
}
Brace counting security
Our algorithms and patterns all implicitly assume that our braces are balanced. Our pattern matching only match balanced braces. Therefore, if we want to flag unbalanced braces as an error then we need a separate mechanism. Fortunately the regular expressions arsenal of tools have exactly such a grouping construct: “(?<close-open> subexpression)” and the grouping is named “Balancing group definition”. This grouping construct deletes the definition of the previously defined group “open” and stores the matched definition in group “close”, this is the interval between the previously defined “open” group and the current group.
Our check for open/close braces is therefore as follows:
@"^((\\{)|(\\})|([^{}]))*(((?<Open>(?<!\\){)((\\{)|(\\})|([^{}]))*)+((?<Close-Open>(?<!\\)})((\\{)|(\\})|([^{}]))*)+)*(?(Open)(?!))$"
“It starts at the beginning of our string, notice the caret (“^”) symbol and ends at the end of our string, notice the dollar sign (“$”) symbol at the end. The caret is followed by our adult pattern: “(\\{)|(\\})|([^{}]))*” matching on anything that is not a brace but allows for an escaped brace, 0 or more times. Next: “((?<Open>(?<!\\){)” defines a group named “Open”, an un-escaped open brace. Then our adult pattern “((\\{)|(\\})|([^{}]))*” matching all non brace characters. Do it for all sequential open braces (simplified version) “(?<Open>{)--all non brace--)+” then match the next close brace with the innermost open brace and remove each group “Open” for which there is a “close” like so: “(?<Close-Open>(?<!\\)})”. The match of all open-close braces is captured in (simplified version): “(((?<Open>{)--all non brace--)+((?<Close-Open>})--all non brace--)+)*”. Lastly, return match or non match based on if we have an “Open” group remaining/missing, like so: “(?(Open)(?!))”.
Familiar and known in the public
I will not review the ProcessXXX() family of routines: ProcessDate(), ProcessForeignKey(), ProcessIf(), ProcessKey() and ProcessLiteral() since they are very similar to the processing explained in the first installment of the article. Please read them and if you have any questions write to me using messages at the end of the article, I will respond. The sophistication added is the replacement of the baby “.*?” with its adult counterpart: “((\\{)|(\\})|[^{}])*?”. A second enhancement is the addition of an interface to the ProcessXXX() family of routines:
public interface IProcessEvaluate
{
bool Evaluate(ConfigElement configElem);
}
Just like in the previous installment, the hierarchy of classes is: ConfigEval is the base class of Config.
ConfigEval
ConfigEval is the meat and potatoes of our evaluation. It has a static constructor (class constructor—“cctor”) where we define the Simple Expression pattern. It also has a constructor (not static constructor—“ctor”) that populates a delegate with the various “Evaluate()” functions:
_evaluateHandler = (new ProcessDate()).Evaluate;
_evaluateHandler += (new ProcessKey(GetConfigElement)).Evaluate;
_evaluateHandler += (new ProcessForeignKey()).Evaluate;
_evaluateHandler += (new ProcessIf()).Evaluate;
_evaluateHandler += (new ProcessLiteral()).Evaluate;
Now let’s review the Evaluate()
function’s signature:
public bool Evaluate(ConfigElement configElem)
Those of you who are familiar with the previous version(s) of .Net will immediately notice that the Evaluate() function returns a value and as such could not be part of a multi-cast delegate. Moreover, even if Evaluate() could be part of a multi-cast delegate the return value will be lost and we need it, this is the mechanism we have, to know if an evaluation has happened or not.
We have a way out of all this mess: .Net 2.0 have introduced the GetInvocationList() function which solves all our problems (it solves our financial problems, marital problems, social problems… well almost all our problems.)
There is another way of doing things, other than employing a multi-cast delegate and employing a GetInvocationList() function. We can call the Evaluate() functions of each of the ProcessXXX classes in sequence. I prefer the multi-cast delegate simply because it provides a place in the beginning of the class, the constructor, to add all the Evaluate() functions, then use a for-loop to cycle through the Evalute() functions. I feel that this is less error prone when time comes for you to add your own ProcessXXX class. Though the advantage of one method (multi-cast delegate) versus the other (calling the Evaluate() functions in sequence) is slight since the only place we need to access the Evaluate() functions is in the EvaluateConfig() function.
EvaluateConfig
private const int PassThroughUpperLimit = 10000;
protected virtual void EvaluateConfig()
{
_evalSuccess = true;
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)
{
for (LinkedListNode<ConfigElement> configNode = configElements.First;
configNode != null; )
{
bool bEval = false;
bool rc = IsSimpleExpression(configNode.Value.Value);
if (rc)
{
foreach (Delegate del in _evaluateHandler.GetInvocationList())
{
rc = (bool)del.DynamicInvoke(new object[] { configNode.Value });
if (rc)
{
bEval = true;
break;
}
}
}
if (!bEval)
{
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;
}
}
As first order of the day, the function builds a linked list of all the config entries needing evaluation. Look at the expression:
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);
Let’s look at the LINQ expression, the collection part of the configNodes
:
from k in _configEntry
where IsSimpleExpression(k.Value.Value)
select k.Value
The expression will retrieve from _configEntry
values for which IsSimpleExpression(..)
is true
—which is exactly what we are looking for.
Now for our evaluation we have a double loop construct. The outer loop keeps vigilant over a runaway infinite iteration process and the second, the inner loop, evaluates the first simple-expression for each of the nodes in the linked list. The inner loop is where the magic happens:
bool bEval = false;
bool rc = IsSimpleExpression(configNode.Value.Value);
if (rc)
{
foreach (Delegate del in _evaluateHandler.GetInvocationList())
{
rc = (bool)del.DynamicInvoke(new object[] { configNode.Value });
if (rc)
{
bEval = true;
break;
}
}
}
if (!bEval)
{
LinkedListNode<ConfigElement> p = configNode.Next;
configElements.Remove(configNode);
configNode = p;
}
else
configNode = configNode.Next;
The code starts by asking if there is a simple expression in the config entry, by employing a call to the IsSimpleExpression(..)
function. If there is a simple expression, then we call our delegate’s Evaluate()
function for each of the ProcessXXX classes in sequence using the GetInvocationList()
construct. The del.DynamicInvoke(..)
will invoke the Evaluate(..)
function according to the sequence order given in the constructor and once evaluated it will move on to the next config-entry (next node).
This scenario will leave an unknown config-entry unharmed and move on.
The EvaluateConfig()
is the workhorse of the ConfigEval
class, I also have two other related methods the one is PreEvaluateConfig()
and the second is PostEvaluateConfig()
. The constructor of the Config class ends by calling the PreEvaluateConfig()
then it calls the EvaluteConfig()
and ends by calling PostEvaluateConfig()
. The PreEvaluateConfig()
is where I placed the check for balanced braces using the call to IsBalancedCurlyBraces()
. The PostEvaluateConfig()
is where I remove the escape (“\”) characters from the escaped braces character sequence.
The finish
Now that it looks like we are done I would like to handle one very rare situation. What if, Heaven help us, you need to have a backslash in-front of a brace in the final version of the config entry. In that case you will need to double the backslash, in the config file, so that one backslash will remain after all that is needed to be removed was removed.
<add key="Heaven Help Us" value="\\{"/>
And you have your open curly that will not be flagged as an error by the PreEvaluateConfig().
Where do we go from here
You may care to add a (key, value) that leaves the expression as is and not attempt an evaluation. You may care to add a “LeaveBe” key (in a {key::value} pair), for example: <add key="As Is" value="{LeaveBe::you may include unbalanced curlies, commas and heaven knows what else, without the worry of condemnation}"/>.
What if you need further processing
For example, you need further processing to decrypt a connection string. All this, as it was done in the past is part in the Config class property that retrieves the connection string. Remember, we had the constructor build the _configEntry using the Attach(..) method. Then each entry in the _configEntry had a property that actually exposes that entry and is evaluated once, Config.ConnectionString property. This is where you will decrypt a connection string or do other processing to the entry.
A time to choose
You now have a choice between this installment and the previous one, as described in the first article, Enhanced Configuration File Handling. The question is which one shall you choose for your project? The first installment is simpler to implement and maintain. So if you do not anticipate the need for an evaluation within an evaluation then the choice is clear, installment I. On the other hand if there is a need for an evaluation within an evaluation, like a need for an if-expression then installment II (the current installment) is your clear choice. If you feel comfortable with this installment then even if you do not need the nesting capabilities you may still choose the second installment for the flexibility it offers. Or simply because this is a standard that you set for yourself. Moreover, you will allow room for expansion in the future even if your needs now are modest.
Epilog
Use the KISS concept (keep is short and simple). We now have a very powerful expandable system that is quite expressive. This is a double edged sword of capabilities. On the one hand it is very expressive—the good side of things. On the other hand you may now create very complicated expressions that are difficult to understand—the dark side. Use your judicious sense between complicated and expressive to short and simple. Using the “if” construct along with a “ForeignKey” construct you may now have a single config file that will not change between Dev, UAT and Prod environments; nice simple and easy to understand.
Enjoy,
Avi
History
- 3/19/2009 Original submission
- 9/12/2009 Announce Article III (Enhanced Configuration File Handling III)