Skip to content

Instantly share code, notes, and snippets.

@vitaliitylyk
Last active February 6, 2020 10:01
Show Gist options
  • Save vitaliitylyk/ac419bc30993e3a6dfe7ed88767eb2ce to your computer and use it in GitHub Desktop.
Save vitaliitylyk/ac419bc30993e3a6dfe7ed88767eb2ce to your computer and use it in GitHub Desktop.
Custom Sitecore configuration section handler, which allows to inject environment variables to Sitecore settings, sc.variables and site settings. More information here: https://blog.vitaliitylyk.com/sitecore-environment-variables-config-builder/
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);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment