5

Background

So long story short. We all know and use (right?) the established Sitecore Best Practice for not modifying vanilla configuration files. We use config patches (or config overrides, as some call them) to add new settings and patch existing ones.

Just so there is no misunderstanding here; this is the type of setting I'm talking about:

<setting name="AllowDuplicateItemNamesOnSameLevel" value="true" />

These all sit under the <sitecore><settings> element and is therefore under the control of the Sitecore Configuration Factory and can thus be patched by config include files that Sitecore loads up dynamically when the factory initialises.

The problem

Sitecore.Configuration.Settings is sealed and GetSetting() is a public static. So there, our challenge is laid out before us.

What I would like to achieve is a way, so these defined settings (patched or vanilla) would get "overruled" if a similarly named setting had been defined by a regular <appSettings> value. And I would like this to be injected in, somehow - shielded from the rest of the application.

The significance of <appSettings>

The reason for this, I should add, is Azure. And for running Sitecore on Azure PaaS. As you may or may not be aware, any WebApp running on Azure can have instance/slot specific configuration settings defined in the Azure Portal. It looks like this.

Settings on Azure Portal

These settings will override any <appSettings> defined in your web.config, and the connection strings will do the same. But it only works for this; not for the Sitecore flavoured settings.

Now I'm not saying all settings should be pushed to the portal. But some, definitely make sense. Any server-to-server connection settings, service bus connection strings, mail servers and so on.

My progress so far

So to try and implement this; I've done what I must. I've opened up dotPeek and taken a look around ;-)

The real work is done in a private static GetWebSettings() method. In short, it maintains an internal Hashtable for all settings, and will loop through all settings/setting XML elements for settings and values. Given that it's a private static, the only way to override it would be some form of method replacement via reflection. While possible, it's not the prettiest of approaches and I'd rather avoid it.

So the question is; where would be the best (err.. least likely to break/cleanest/least invasive) place to inject code that would allow these settings to be overridden by Azure? I haven't written that code yet, but something to the effect of this:

var setting = GetSettingFromSitecore(settingName);
if (Azure.Settings[settingName])
    setting = Azure.Settings[settingName];

Any ideas?

  • What's your deployment toolchain? It's common to use XML transforms based on build configuration, or to use Octopus Deploy variable injection to achieve this sort of thing – Richard Hauer Mar 15 '17 at 1:32
  • It is common, indeed. However, I perceive Azure as a much more "fluid" environment, where additional resources can be provisioned on-demand or as-needed, and I believe it falls within the realm of the Azure administrator to freely work with the tools he or she has available. That would include the freedom to reassign resources without requiring a re-deploy or release or even involving the dev team. – Mark Cassidy Mar 15 '17 at 8:22
  • For sure - in which case your only approach is a Decorator for the config system that patches in appSettings from the environment over the top of the Sitecore settings, which is largely what your doing here I suppose. As @Zachary is saying below, this system isn't really designed to be extended - it's the last bastion of hard-wired Sitecore. Maybe this is worth taking to the product team for the next release. It should be pretty easy to unseal these classes. – Richard Hauer Mar 16 '17 at 0:14
2

Let me start off by saying that I'm not happy with either of the options that I came up with, but I'm not sure how else you'd do it, so I thought I would at least get them recorded.

The first thing that I had thought of for this was to patch in your settings after the ConfigReader finishes patching in everything else, like so:

public class ConfigReader : Sitecore.Configuration.ConfigReader
{
    protected override void LoadAutoIncludeFiles(XmlNode element)
    {
        // patch in files like normal, first
        base.LoadAutoIncludeFiles(element);

        // patch in your settings, here
    }
}

The problem is that your <appSettings> aren't stored in Sitecore config patch files, presenting a bit of a challenge with regards to patching.

This leaves us with two options:

  1. Write the XML for the settings programmatically and patch it in using your extended ConfigReader (above) and an extended ConfigPatcher
  2. For each of your <appSettings>, create a new SettingsSwitcher instance, effectively overriding the setting in the config

Neither of these options are super pretty, but let's have a closer look at their pros and cons:

Option 1: Writing the XML for and Patching Each appSetting

Since you already have an overridden ConfigReader (above), start by extending the ConfigPatcher class, and add a PatchNode(XmlNode) method to it with the job of patching in a single XmlNode passed in as an argument.

Next, add an override for the ConfigReader.GetConfigPatcher(XmlNode) method to your extended ConfigReader class, and have it return a new instance of your extended ConfigPatcher class instead of the native Sitecore ConfigReader class.

Finaly, add a call to your new YourPatcher.PatchNode(XmlNode) method to the end of your overridden LoadAutoIncludeFiles(XmlNode) method, above.

Pros

  1. Since you are creating the XML and using it to patch the setting, it is treated like any other XML patch and will be displayed in the ShowConfig
  2. Little to no risk of side-effects
  3. There is no chance of the override value being "lost" without the AppPool recycling

Cons

  1. You will need to retrieve the appSettings as a NameValueCollection and build the XML manually, with the appropriate <patch:attribute /> and/or <setting set:...> settings, the parent <sitecore> node and the containing <configuration> node with the correct XML namespaces set on it. This isn't a lot of work, but it does make the implementation a bit dirtier than it already is.
  2. Less maintainable, as you are extending ConfigReader and ConfigPatcher and overriding several methods between the two of them. If/when you go to do a Sitecore upgrade, you will need to ensure that any changes to these classes and their methods (especially those that you are overriding) have been accounted for

Option 2: Using the SettingsSwitcher to override your settings' values

This option is a little bit more elegant, though it isn't without its faults. For this option, you will need to do the following:

foreach(var key in System.Configuration.ConfigurationManager.AppSettings.AllKeys)
{
    new SettingsSwitcher(key, System.Configuration.ConfigurationManager.AppSettings[key]);
}

The reason why this works is because by instantiating the SettingsSwitcher with a setting (name and value) you are adding that setting's name and value to the stack. Every time Sitecore.Configuration.Settings.GetSettings<T>(...) is called, it first tries to get the setting value from the SettingsSwitcher and if the value is set there then that is the value that is used. Effectively, you will have overridden the setting in the config files.

Note: in an actual implementation, I would namespace the appSettings that should be brought into Sitecore, so that you could have straight .NET ones, as needed. Of course, this is an implementation decision, so I didn't include this in my solution.

Pros

  1. More elegant solution - no manual generation of XML patch files
  2. No extending/overriding native Sitecore classes and methods, so no trouble with upgrades, so long as the reliance on the SettingsSwitcher doesn't change
  3. Far less code and less effort

Cons

  1. Overridden settings are not reflected in the ShowConfig
  2. If anything happens to the stack then your overridden settings could be lost

Summary

Honestly, I'm not super happy with either of these options, but I don't yet see another way to accomplish what you are looking for.

My recommendation would be to go with option 1, since it is less likely for something to go wrong and you can see the overridden settings in the ShowConfig. When you go to upgrade, if necessary you can change to the implementation in option 2.

I like option 2 more, from a code-cleanliness and effort standpoint, but at the end of the day I think it is less reliable then option 1, and I don't like the idea of overridden values not showing up in the ShowConfig.

1

A year on, and I've just found this after re-trying to solve the same problem, primarily for overriding the SolrBaseAddress setting with a setting from AppSettings, as this will allow a Sitecore 9 deployment into Azure PaaS without needing to use any Sitecore patch files at all. It's not a generic solution, in that it will only work for Solr Settings as described, but there's likely an adaptation that could be used for other settings, I just haven't checked yet.

Enter Sitecore.ContentSearch.ContentSearchManager::Locator

This static property is the "DI container" for a number of the services that are used inside the Content Search subsystem. The one we're interested in today is the registration for ISettings.

1. Implement ISettings

The ISettings object is (annoyingly) implemented by an internal class called Sitecore.CotentSearch.Abstractions.SettingsWrapper. It's very frustrating that Sitecore have chosen to mark so many classes as internal, but onwards and upwards. This class is a wrapper around the static properties and methods of Sitecore.Configuration.Settings so you'll need to start by creating you're own. I suggest judicious use of copy/paste from your favourite decompiler.

1a. Intercept Sitecore.Configuration.Settings::GetSettings()

Once you've got a new implementation of the ISettings object you will need to intercept the ISetting::GetSetting(string) and ISetting::GetSetting(string,string) methods to first look in AppSettings to see if your setting is there, and if not, return control to the Sitecore.Configuration.Settings version.

public string GetSetting( string name )
{
    if ( ConfigurationManager.AppSettings.AllKeys.Contains( name ) )
        return ConfigurationManager.AppSettings[name];

    return Settings.GetSetting( name );
}
public string GetSetting( string name, string defaultValue )
{
    if ( ConfigurationManager.AppSettings.AllKeys.Contains( name ) )
        return ConfigurationManager.AppSettings[name];

    return Settings.GetSetting( name, defaultValue );
}

2. Override the service locator registration for ISettings

Untested I assume you can do this with an <initialize /> pipeline component, but put it in right at the beginning, before any of the Solr setup has happened. The registration is all done as part of the Locator's getter by default, so there's no special sequencing required other than just replacing the registration like so:

public void Process(PipelineArgs args)
{
    ContentSearchManager.Locator.Unregister( typeof( ISettings ) );

    var fallbackSettingsService = new FallbackSettingsService();
    ContentSearchManager.Locator.Register<ISettings>( c => fallbackSettingsService );
}

Or something like this anyway - I haven't tested this bit yet.

I'm not 100% sure when this setup came into being - likely as part of 8.2 when the DI components materialised in Sitecore. They are definitely available in the 9.0.2 version I am deploying today.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.