Does your solution have one or more Z folders in the App_Config/Include directory? The answer is probably yes. And in those folders are probably some files that start with a Z. Most of you know why: Sitecore loads config files in a folder first, then recurses down the folder chain until it’s done. That’s the “preferred” method according to Sitecore.
I think we can do better, though. I was thinking this through while sitting in the airport due to a delayed flight. It could be done, but it would require a little more tweaking than I think some Sitecore devs are accustom to. But that’s ok, right? No pain, no gain.
Enter in the Sitecore Config Deferrer. It’s a simple concept really. Allow config files to not be processed in alphabetical orders. Allow them to be processed in an order that makes sense for your project. 90% of the time this isn’t an issue, but for that 10%, adding Zs or folders to affect the logic is a bit clunky. Again, we can do better.
So here’s the solution I came up with.
Take a set of config files.
From a config in your App_Config/Include/Folder/PatchFolder.config:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <settings> <setting name="demoSetting"> <patch:attribute name="value" value="folderValue" /> </setting> </settings> </sitecore> </configuration>
From the root of your App_Config/Include/PatchRoot.config
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <settings> <setting name="demoSetting"> <patch:attribute name="value" value="rootValue" /> </setting> </settings> </sitecore> </configuration>
Nothing too complicated there. They both are competing for the same setting. In the “files first, folders second” default behavior, the output would look something like this:
<setting name="demoSetting" patch:source="PatchFolder.config" value="folderValue"/>
That makes sense. We see that all the time. That’s why zSwitchMasterToWeb.config is in a zzz folder.
Now we can look at a simple configuration file added to our project:
FileList> <File name="~/App_Config/Include/PatchRoot.config"/> </FileList>
With this file in place, this is our output:
<setting name="demoSetting" patch:source="PatchRoot.config" value="rootValue"/>
Did we change any of the files in the Includes folder? Nope.
Here’s how we do the magic:
In the web.config, there’s a configuration section. This configSection is responsible for loading all the includes and applying the patch process at startup:
<configSections> <section name="sitecore" type="Sitecore.Configuration.ConfigReader, Sitecore.Kernel" /> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, Sitecore.Logging" /> </configSections>
Notice the first section there. It tells Sitecore that for loading this section, use the default ConfigReader. This reader does exactly what we know it to do. Parse all files/folders and apply patches. This is what we need to change to add in the ability to defer configs.
Here’s a look at the LoadAutoIncludes method that ships with Sitecore. I’ve highlighted the additions.
protected override void LoadAutoIncludeFiles(XmlNode element) { Assert.ArgumentNotNull(element, "element"); ConfigPatcher configPatcher = GetConfigPatcher(element); LoadAutoIncludeFiles(configPatcher, MainUtil.MapPath("/App_Config/Sitecore/Components")); LoadAutoIncludeFiles(configPatcher, MainUtil.MapPath("/App_Config/Include")); ProcessDeferrals(configPatcher); }
The call to the ProcessDeferrals is the only new item in this method. We’ll talk about him later. The real meat of this code comes inside the overloads where you pass in a path and patcher:
protected override void LoadAutoIncludeFiles(ConfigPatcher patcher, string folder) { Assert.ArgumentNotNull(patcher, "patcher"); Assert.ArgumentNotNull(folder, "folder"); if (_configDefers == null) { InintializeDeferrals(); } try { if (!Directory.Exists(folder)) return; foreach (string str in Directory.GetFiles(folder, "*.config")) { try { if ((File.GetAttributes(str) & FileAttributes.Hidden) == 0) { if (_configDefers != null) { var configItem = _configDefers.Find(f => f.Equals(str, StringComparison.InvariantCultureIgnoreCase)); if (configItem != null) { //We found the item based off path. Skip loading it for now if (File.Exists(configItem)) { Log.Info("Deferring config: " + str, this); continue; } } } patcher.ApplyPatch(str); } } catch (Exception ex) { Log.Error(string.Concat("Could not load configuration file: ", str, ": ", ex), typeof(Factory)); } } foreach (string str in Directory.GetDirectories(folder)) { try { if ((File.GetAttributes(str) & FileAttributes.Hidden) == 0) LoadAutoIncludeFiles(patcher, str); } catch (Exception ex) { Log.Error(string.Concat("Could not scan configuration folder ", str, " for files: ", ex), typeof(Factory)); } } } catch (Exception ex) { Log.Error(string.Concat("Could not scan configuration folder ", folder, " for files: ", ex), typeof(Factory)); } }
The first part is there to initialize our list of deferred files. This is a relatively simple method:
private void InintializeDeferrals() { Log.Info("Initializing config defer", this); _configDefers = new List<string>(); var configName = HttpContext.Current.Server.MapPath("~/App_Config/ConfigsToDefer.config"); if (File.Exists(configName)) { Log.Info("Found config defer file at: " + configName, this); try { var deferConfig = new XmlDocument(); deferConfig.Load(configName); var configNodes = deferConfig.SelectNodes("/FileList/File"); if (configNodes != null) { foreach (XmlNode configNode in configNodes) { if (configNode.Attributes != null && configNode.Attributes["name"] == null) { Log.Warn("Couldn't find name on deferrable config item", this); continue; } if (configNode.Attributes != null && configNode.Attributes["name"] != null) _configDefers.Add(HttpContext.Current.Server.MapPath(configNode.Attributes["name"].Value)); } } } catch (Exception ex) { Log.Error("Couldn't load deferrable config from: " + configName, ex, this); } } else { Log.Warn("Couldn't find defer config at: " + configName, this); } }
Read in the XML, iterate through the File items, resolve to absolute paths, and be done with it.
The next step is to actually skip the files. If we find a file while we’re trying to iterate all folders/files, then we just skip it. Easy enough.
Finally, all those files that were skipped need to be applied. That’s our ProcessDeferrals method. It does even less than you think it would:
private void ProcessDeferrals(ConfigPatcher configPatcher) { Assert.ArgumentNotNull(configPatcher, "configPatcher"); foreach (var config in _configDefers) { if (File.Exists(config)) { Log.Info("Applying Deferred config: " + config, this); configPatcher.ApplyPatch(config); } else { Log.Warn("Skipping config. Doesn't exist. Config file: " + config, this); } } }
That’s it. All that’s left to do is patch in the web.config manually. We can’t do this automatically, unfortunately. You need to touch the web.config by hand. It’s ok though, I promise.
<configSections> <section name="sitecore" type="Sitecore.SharedSource.DeferringConfigReader.DeferringConfigReader, Sitecore.SharedSource.DeferringConfigReader" /> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, Sitecore.Logging" /> </configSections>
To summarize, we do the following:
- Update our web.config to point to the new Reader
- Initialize our deferral list.
- Iterate through all files/folders like normal.
- If we find a file to defer, skip it.
- Process all deferred files.
- ???
- Profit.
I’ve posted the source for this solution out in bitbucket: https://bitbucket.org/RAhnemann/deferringconfigreader
If you have any questions or run into any issues, please let me know!
-Rob