Inspired by ASP.NET configuration builders I thought: wouldn’t it be awesome to be able to modify Sitecore configuration via environment variables in a similar way? This is especially relevant in Docker world.
As I’ve mentioned before, to be a good Docker citizen , a containerized application has to support configuration via environment variables.
Out of the box, Sitecore supports $(env:ENVIRONMENT_VARIABLE_NAME)
syntax to use environment variables in your configuration. However, this has 2 limitations:
- This syntax is not supported for Sitecore variables (
sc.variable
) - Before being able to actually use environment variables, you first have to modify all places where you want to use them with
$(env:ENVIRONMENT_VARIABLE_NAME)
syntax.
My goal was to be able to modify Sitecore configuration via environment variables without making any changes to Sitecore config files themselves. In particular:
- Update settings
- Update variables
- Update site settings (e.g set hostName, scheme, etc)
This syntax would look something like this (on the left is an environment variable name):
SITECORE_SETTINGS_<settingname>=<value>
SITECORE_SITES_<sitename>_<attributename>=<value>
SITECORE_VARIABLES_<variablename>=<value>
For example:
SITECORE_SETTINGS_MEDIA.MEDIALINKSERVERURL=https://www.hostname.com
SITECORE_SITES_WEBSITE_HOSTNAME=hostname.com
At first I considered to follow Microsoft approach and develop a custom configuration builder. However, ASP.NET configuration builders intrude before Sitecore does it’s magic with aggregating all include files. Therefore, this approach will not allow to inject environment variables into include files, which is very limiting.
So I ended up with customizing the Sitecore configuration section handler. By default, Sitecore config section is defined in Web.config in the following way:
<configSections>
...
<section name="sitecore" type="Sitecore.Configuration.RuleBasedConfigReader, Sitecore.Kernel" />
...
</configSections>
This can be easily customized via pointing the “sitecore” section handler to a custom class, which inherits from RuleBasedConfigReader
. Below you can find an example of a custom handler which loops over environment variables and injects them into Sitecore configuration, if it matches the appropriate setting
/sc.variable
or site setting
(the comparison is case-insensitive). In case of a match the patch:sourceEnvironmentVariables
attribute is appended to an appropriate XML node to be able to easily track changes in Sitecore’s showconfig.aspx
.
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Xml; | |
namespace VitaliiTylyk.Configuration | |
{ | |
/// <summary> | |
/// Allows to implicitly replace values in sitecore/settings and sitecore/sites section from | |
/// environment variables. This is mimicking the behaviour of ASP.NET Configuration Builders (https://docs.microsoft.com/en-us/aspnet/config-builder). | |
/// In case environment variable is found and injected, patch:sourceEnvironmentVariables attribute is appended to Xml node. | |
/// | |
/// Supported syntax is (keys are case insensitive): | |
/// | |
/// 1) SITECORE_SETTINGS_<settingname>=<value> | |
/// 2) SITECORE_SITES_<sitename>_<attributename>=<value> | |
/// 3) SITECORE_VARIABLES_<variablename>=<value> | |
/// | |
/// Examples: | |
/// | |
/// SITECORE_SETTINGS_MEDIA.MEDIALINKSERVERURL=http://mysite.com | |
/// SITECORE_SITES_CONTENTAPI_SCHEME=http | |
/// </summary> | |
public class EnvironmentVariablesConfigReader : Sitecore.Configuration.RuleBasedConfigReader | |
{ | |
private const string PatchSourceAttributeName = "patch:sourceEnvironmentVariables"; | |
private const string ValueAttributeName = "value"; | |
protected override XmlDocument DoGetConfiguration() | |
{ | |
var configuration = base.DoGetConfiguration(); | |
InjectEnvironmentVariables( | |
variableNamePrefix: "SITECORE_SETTINGS_", | |
key => configuration.SelectSingleNode($"/sitecore/settings/setting[{XPathCompareCaseInsensitive("name", key)}]"), | |
key => ValueAttributeName, | |
// Some Sitecore settings have their XML element inner text set instead of the "value" attribute, for example PublishingServiceUrlRoot. | |
// In such cases we need to set the element inner text as well. | |
setInnerTextWhenTargetNotFound: true); | |
InjectEnvironmentVariables( | |
variableNamePrefix: "SITECORE_SITES_", | |
key => configuration.SelectSingleNode($"/sitecore/sites/site[{XPathCompareCaseInsensitive("name", key.Split(new char[] { '_' })[0])}]"), | |
key => key.Split(new char[] { '_' })[1]); | |
return configuration; | |
} | |
protected override void ReplaceGlobalVariables(XmlNode rootNode) | |
{ | |
rootNode = rootNode ?? throw new ArgumentNullException(nameof(rootNode)); | |
// Before Sitecore replaces variables, replace their values from environment variables, if found | |
InjectEnvironmentVariables( | |
variableNamePrefix: "SITECORE_VARIABLES_", | |
key => rootNode.SelectSingleNode($"/sitecore/sc.variable[{XPathCompareCaseInsensitive("name", key)}]"), | |
key => ValueAttributeName); | |
base.ReplaceGlobalVariables(rootNode); | |
} | |
private static IDictionary<string, string> GetEnvironmentVariables(string prefix) | |
{ | |
var typedDictionary = new Dictionary<string, string>(); | |
foreach (DictionaryEntry item in Environment.GetEnvironmentVariables()) | |
{ | |
// Environment variable names should be case-insensitive, so we uppercase them for simplicity | |
var key = ((string)item.Key).ToUpperInvariant(); | |
if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) | |
{ | |
typedDictionary.Add(key, (string)item.Value); | |
} | |
} | |
return typedDictionary; | |
} | |
private static void InjectPatchSource(XmlNode node, string source) | |
{ | |
var valueSourceAttribute = node.Attributes[PatchSourceAttributeName] | |
?? node.OwnerDocument.CreateAttribute(PatchSourceAttributeName, "http://www.sitecore.net/xmlconfig/"); | |
valueSourceAttribute.Value = string.IsNullOrEmpty(valueSourceAttribute.Value) | |
? source | |
: valueSourceAttribute.Value += $", {source}"; | |
node.Attributes.Append(valueSourceAttribute); | |
} | |
private static string XPathCompareCaseInsensitive(string attributeName, string value) | |
{ | |
// Based on https://stackoverflow.com/questions/30530274/select-node-with-attribute-case-insensitive-via-xpath | |
return $"translate(@{attributeName}, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') = '{value.ToUpperInvariant()}'"; | |
} | |
private void InjectEnvironmentVariables( | |
string variableNamePrefix, | |
Func<string, XmlNode> nodeSelector, | |
Func<string, string> targetAttributeNameSelector, | |
bool setInnerTextWhenTargetNotFound = false) | |
{ | |
var environmentVariables = GetEnvironmentVariables(variableNamePrefix); | |
foreach (var environmentVariable in environmentVariables) | |
{ | |
var key = environmentVariable.Key.Replace(variableNamePrefix, string.Empty); | |
var node = nodeSelector(key); | |
if (node == null) | |
{ | |
continue; | |
} | |
var targetAttributeName = targetAttributeNameSelector(key); | |
var attributeFound = false; | |
foreach (XmlAttribute attribute in node.Attributes) // Looping through all of them to perform case-insensitive lookup | |
{ | |
if (attribute.Name.Equals(targetAttributeName, StringComparison.OrdinalIgnoreCase)) | |
{ | |
attribute.Value = environmentVariable.Value; | |
attributeFound = true; | |
InjectPatchSource(node, environmentVariable.Key); | |
break; | |
} | |
} | |
if (!attributeFound && setInnerTextWhenTargetNotFound) | |
{ | |
node.InnerText = environmentVariable.Value; | |
InjectPatchSource(node, environmentVariable.Key); | |
} | |
} | |
} | |
} | |
} |
And this is an output from showconfig.aspx
for the Media.MediaLinkServerUrl
Sitecore setting:
<setting name="Media.MediaLinkServerUrl" value="http://hostname.com" patch:sourceEnvironmentVariables="SITECORE_SETTINGS_MEDIA.MEDIALINKSERVERURL"/>
We are successfully using this approach for a while in our project in combination with Docker. Being able to set any Sitecore setting without patching config files greatly simplifies the configuration management.