I wasted a few hours recently when I did something which seemed entirely reasonable with Rule-Based Config in Sitecore and it did not work the way I thought it would. Here's an explanation of what I did and what happened as a result, so you can avoid making the same mistake as me...
I was working in adding some custom configuration for a docker-based Sitecore site. I needed to add a new config rule to enable and disable some configuration based on an environment variable. This is a fairly common development pattern for Sitecore, so it's usually not too tricky to set up.
I added an AppSetting for the rule definition variable in the
web.config
and gave it a default value. The config was to enable or disable a particular behaviour, so using "true" and "false" seemed sensible.
<appSettings> <add key="testVariable:define" value="True"/> </appSettings>
And I added a config patch that would use this variable: (This isn't the config I was using - but it's a "minimal recreation of the issue" instead - to keep this post more readable)
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:testVariable="http://www.sitecore.net/xmlconfig/testVariable/"> <sitecore> <testIsTrue testVariable:require="True" /> <testIsFalse testVariable:require="False" /> </sitecore> </configuration>
I added an environment variable mapping to the
docker-compose.override.yml
using the
SITECORE_APPSETTINGS_
prefix which means this value gets mapped automatically into the corresponding App Setting:
cm: environment: SITECORE_APPSETTINGS_testVariable:define: ${TESTVARIABLE}
And then added a value to the
.env
file to set a value for this variable so Docker would get a value suitable for developers:
TESTVARIABLE=False
All pretty run-of-the-mill config for Sitecore in containers I thought...
I fired up the containers and loaded
showconfig.aspx
to check what my config patch had done. My expectation was that I'd see a
<testIsFalse/>
element in the config because the environment value had overridden the default and set my flag to false. But what I saw was this:
The result of the config patch was that both of the values appeared in the config:
<testIsTrue patch:source="Test.config"/> <testIsFalse patch:source="Test.config"/>
This did not make a great deal of sense to me initially. So my first instinct was to check what had happened with the environment variable. Was that wrong? So I tried running some PowerShell in the CM container to check:
Which looked correct. And to see what had happened with the actual
web.config
value I tried a bit of ASP.Net code to show the value of the
testVariable:define
AppSetting. And this bit of code:
<%=ConfigurationManager.AppSettings["testVariable:define"] %>
Gave the expected response based on the
.env
file above overriding the default in the
web.config
:
This continued not to make a great deal of sense to me. These values matched what the environment file contained.
So I tried varying the value a bit and checking what happened:
testVariable Value | testIsTrue element | testIsFalse element |
---|---|---|
Empty string | In config file | Not in config file |
True | In config file | Not in config file |
False | In config file | In config file |
So passing "true" looks like it worked ok, but both empty string and "false" gave unexpected results.
At this point I scratched my head and decided to try a test. I changed my config to use "yes/no" instead of "true/false". The config patch changed to look like:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:testVariable="http://www.sitecore.net/xmlconfig/testVariable/"> <sitecore> <testIsTrue testVariable:require="Yes" /> <testIsFalse testVariable:require="No" /> </sitecore> </configuration>
And tried my tests again:
testVariable Value | testIsTrue element | testIsFalse element |
---|---|---|
Empty string | Not in config file | Not in config file |
True | In config file | Not in config file |
False | Not in config file | In config file |
And that looks like exactly the behaviour I would expect from rule-based config.
So my rules weren't wrong - but there was something odd about how the patching engine was handling my original rules.
I had always assumed that these config rules just used basic string matching - It looked for fairly literal matches between the value in the App Settings and the values you used in your rules. But this suggests that may not be true.
So I put on my nerd hat, and dropped
Sitecore.Kernel.dll
onto
IL Spy
and started digging into how this code actually works.
At the top of the tree is
Sitecore.Configuration.ConfigReader.DoGetConfiguration()
. That goes through and applies all the patches you have in your
App_Config/Include
folder in turn. Drilling down from there, it's doing stuff like loading individual patch files and looking for rules to evaluate. And eventually the logic appears to end up in the
RuleBasedXmlPatchHelper
class, and its
MatchesConfiguredRoles()
method:
protected virtual bool MatchesConfiguredRoles(string requiredRoleStatement, string[] rules) { if (rules == null || rules.Length == 0 || string.IsNullOrEmpty(requiredRoleStatement)) { return true; } IEnumerable<Token> tokens = GetTokenizer(requiredRoleStatement, rules).Tokenize(); return new Parser(tokens).Parse(); }
Each time the code hits a rule-based config attribute in the form
variable:require="<something>"
, the value of that attribute gets passed into this method as the
requiredRoleStatement
parameter. And the
rules
array contains the set of defined variable values for running rules from the App Setting named
variable:define
. And when this runs, the boolean result that comes back from this is passed to the
ShouldPatchNode()
method, which controls if this rule gets applied or not.
The call to
GetTokenizer().Tokenize()
seems to be the interesting bit here. It takes in our element's rule-based config attribute, plus the array of values for the related config variable. And then it transforms your rule attribute into a list of tokens which describe the boolean logic of the expression. That's probably not wildly clear, but it works like this:
If your
variable:define
App Setting contains the value "A, B", and your XML config element has the rule
variable:require="A OR C"
then you get the following:
The call to
GetTokenizer()
receives "A OR C" for
requiredRoleStatement
and an array of "A" and "B" for
rules
. And the return value's set of
Token
objects looks like:
So "tokenizing" has taken each of the "tokens" in the phrase "A OR C" and turned them into one of two things: A true/false value to say if the value exists in the "rules", or tokens for any boolean logic operators (like brackets and and/or/not). So you end up with a set of tokens which can be evaluated into a single boolean value.
The screen grab above has three tokens:
TrueToken
that says "The value A does exist in the collection of App Setting values"OrToken
FalseToken
that says "The value C does not exist in the collection of App Settings values"And then this gets sent to the
Parser
object, which will evaluate "True Or False" to get "True" - and our config patch will be applied.
And hidden in here is the reason why we get the odd behaviour I found at the top of this post...
Looking deeper into the behaviour of
GetTokenzier()
I found an answer. The parsing loop here has a call to a
ParseKeyword()
where it finds a string that could be a token and then has a
switch
statement to decide what to do with it:
switch (potentialKeyword) { case "true": return new TrueToken(); case "false": return new FalseToken(); case "and": return new AndToken(); case "or": return new OrToken(); }
So if you use the string token
"true"
or
"false"
in your
variable:require
expression, it doesn't get treated as a string! It gets turned into one of the
TrueToken
or
FalseToken
objects we saw in the screen-grab above. And that breaks the logic we'd expect from string matching, because now it's not evaluating "does the
variable:require
token match the
variable:define
token", it's evaluating "is this value boolean true" instead.
If I change my code test above to have a "variable:require" rule that says "true OR false" but leave the App Setting for "variable" as "A, B" then the tokenizer does this:
This rule should
absolutely
evalueate as false - our
variable:require
tokens do not exist in the App Setting. But because the parser treats "True" and "False" as actual boolean values not strings, we get a tokenizer result that is
TrueToken
OrToken
FalseToken
- which will evaluate as true!
And when I test this with the specific values I'd used in my original example, I see this:
And that odd behaviour seems to match up with exactly the odd behaviour that started me off down this path.
I raised a support ticket about this, and Sitecore Support have confirmed this as a bug. If this is a problem for you too, you can use the reference
599521
to refer to this when you're talking to support. I've also raised this with the documentation team, as it may be worth updating the public docs to help others avoid this issue.
Despite this issue having popped up for me in a container-based site, I don't believe this has anything to do with containers. I'm pretty sure that you'll see exactly this behaviour in a non-container site too. And likely in any version of Sitecore which includes the rule base config too.
So having dug into this I have a simple recommendation to make for now:
Don't use "true" or "false" in your Rule-Based Config expressions. You want to give things names that are not boolean literals - so something like "yes" and "no" or "active" and "inactive" instead will work the way you expect.
Otherwise there's a good chance your config will not get evaluated the way you expect...
↑ Back to top