Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2023/rule-based-config-issue

Tripped up by boolean values in Rule-Based Config

I thought this just did string matching, but it seems not...

Published 09 October 2023

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...

What I tried... url copied!

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...

What happened? url copied!

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 output of Sitecore's ShowConfig.aspx page, showing that both of the patch elements are included in the final configuration.

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:

A powershell session inside the Sitecore CM container, showing that the SITECORE_APPSETTINGS_testVariable:define App Setting value is false.

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:

A browser window showing the runtime output of the code above - the value 'False'.

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.

Confusion reigns... url copied!

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.

Digging into this a bit... url copied!

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:

LinqPad running the call to Tokenizer.Tokenize(), showing the resulting tokens for the rule 'A OR C' when the AppSetting is 'A, B'

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:

  • A TrueToken that says "The value A does exist in the collection of App Setting values"
  • An OrToken
  • A 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...

An answer! url copied!

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:

LinqPad running the call to Tokenizer.Tokenize(), showing the resulting tokens for the rule 'True OR False' when the AppSetting is 'A, B'

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:

LinqPad running the call to Tokenizer.Tokenize() twice, showing the resulting tokens for the rule 'True' when the AppSetting is 'False' and when the rule is 'False' and the AppSetting is 'False'

And that odd behaviour seems to match up with exactly the odd behaviour that started me off down this path.

Conclusions... url copied!

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