Advanced Sitecore Config Strategies for Multi-Site Applications

June 22, 2017

Blog | Strategy | Advanced Sitecore Config Strategies for Multi-Site Applications

Coming up with a multi-site configuration strategy for Sitecore can be difficult. Ask any seasoned Sitecore developer- config files can make or break a Sitecore solution.  There are many resources available to explain how to patch custom processors, events or even full pipelines into Sitecore. While these resources lay out the basic principles, they often fall short of providing a strategy when and how to implement configs in a robust, manageable format for large multi-site instances. This post provides instructions for architecting a complex multi-site configuration strategy.

To start off, there are a few prerequisites for this blog post that are not detailed here.

  1. The Visual Studio solution that houses the Sitecore application is built and pushed to a separate web root via TDS or Visual Studio Publishing
  2. SlowCheetah or Tokens are used to alter configuration values for various environments
  3. Native Sitecore config files are not included in the Solution.  Exceptions are made for the following files: Web.config and App_Config\ConnectionStrings.config

Foundational Configs

Every Sitecore application should have at least one foundational config file.  The intent of this file is to set high-level settings used by all sites within a multi-site instance.  Typical patches for this file would include (but are not limited to):

  • Data Folder location
  • Global settings
    • Performance Counters
    • Global Caches
    • Not Found URLs
    • etc.

Changes made in this file are not site-specific as many other configs in this blog will outline.  In some circumstances, it may be appropriate to have several foundational config files.  In this case, it is best to prefix each config with its appropriate role, e.g. Cache.Foundation.config, Performance.Foundation.config, etc.

Nest Configs Properly

Many Sitecore developers understand that all configuration files included in App_Config\Include are aggregated by the Sitecore config processing engine at app_start.  What isn’t specifically obvious is that this processing engine reads and processes config files alphabetically, recursively.  Once this concept is understood, it can be used advantageously.  With this in mind, we can construct the following folder structure to help organize a multi-site environment:

  • App_Config
    • Include
      • zSite
        • zSite.MyFeature.config
        • zSite1 (Folder)
        • zSite2 (Folder)
        • zzFoundation.config

The structure is assumed to be within a Visual Studio solution, with accompanying tranforms for each environment, as needed.  All custom configs are, at a minimum, within the App_Config\Include\zSite folder.  This ensures all of our patches will be applied after Sitecore has aggregated all native configs, shared source modules and support configs before it attempts to aggregate our custom configs.  With this approach, we can visit http://oursite/sitecore/admin/showconfig.aspx and know exactly where our custom config element will be patched.  Without this format, there is a chance our config will be patched at an unexpected time and produce unexpected results.

The foundational config is intentionally labeled with two zz’s to ensure it runs after all custom logic has ran. With Sitecore configs, a “last is first” mentality is a great way to look at where a patch will occur.  The last config to make a patch:after a specific element, will ensure the patched element is truly the next element after the requested element.

Site-specific configs are included in their own site folder.  This provides immediate context for which site a feature is targeting.

Create Custom Configs for Each Logical Feature

When creating custom config files, avoid configs such as MySite.Processors.Config or MySite.Events.Config. This is NOT the technique that Sitecore uses for its native configs, for good reason.  This can end up splitting the logic of a feature into multiple configs.  When a feature is erring, the quickest action is to append “.disabled” to the config.  Logically grouping components in this manner, can therefore quickly quell a feature from ruining a production site.

The proper solution is to create configs like:

  • App_Config\Include\zSite\MySite\MySite.SAP.Integration.config
  • App_Config\Include\zSite\ErrorProcessing.config.

Use Variables to Group Actions for Multiple Sites

On a large multi-site instance, it is common to be able to group sites together.  The sites within each group need to have the same functionality across them.  Take for example, marketing sites and brand sites.  Assume there are 3 marketing sites and 5 brand sites, with an additional 6 brand sites slated to come online in the future.  Assume each group of sites has a specific set of processors that must run:

Marketing:

<processor type="My.Processor.Marketing.Redirects,My.Processor" />
<processor type="My.Processor.Marketing.ResolvePointer,My.Processor" />
<processor type="My.Processor.Marketing.CaptureLead,My.Processor" />

Brand:

<processor type="My.Processor.Brand.ErrorStatus,My.Processor" />
<processor type="My.Processor.Brand.ErrorPage,My.Processor" />

To most efficiently enable certain processors for certain sites, the best approach is to create a variable for each group.

<sc.variable name="marketingSites" value="msite1, msite2, msite3" />
<sc.variable name="brandSites" value="bsite1, bsite2, bsite3, bsite4, bsite5" />

The names correspond to the site name definition within the <sites> element.  With these variables created, we can now apply them to our individual processors as follows:

Marketing:

<processor sites="$(marketingSites)" type="My.Processor.Marketing.Redirects,My.Processor">
  <allowedSites>$(sites)</allowedSites>
</processor>
<processor sites="$(marketingSites)" type="My.Processor.Marketing.ResolvePointer,My.Processor">
  <allowedSites>$(sites)</allowedSites>
</processor>
<processor sites="$(marketingSites)" type="My.Processor.Marketing.CaptureLead,My.Processor">
  <allowedSites>$(sites)</allowedSites>
</processor>

Brand:

<processor sites="$(brandSites)" type="My.Processor.Brand.ErrorStatus,My.Processor">
  <allowedSites>$(sites)</allowedSites>
</processor>
<processor sites="$(brandSites)" type="My.Processor.Brand.ErrorPage,My.Processor">
  <allowedSites>$(sites)</allowedSites>
</processor>

NOTE: The above is a little known and non-documented feature.  The variable must be set as an attribute on the processor. It can then be used as a local variable to that processor.  Only then is it understood properly by Sitecore when the processor is firing.  In other words, you cannot shortcut this method by using:

<processor type="My.Processor.Brand.ErrorPage,My.Processor">
  <allowedSites>$(brandSites)</allowedSites>
</processor>

In the example above, the value for the property allowedSites will be the string, $(brandSites). However, when implemented properly (first as an attribute on the processor, and then used as a local variable) the variable is translated to the allowedSites property correctly.

<processor sites="$(brandSites)" type="My.Processor.Brand.ErrorPage,My.Processor">
  <allowedSites>$(sites)</allowedSites>
</processor>

Somewhat interesting is that ShowConfig.aspx will not output the local variable correctly, but as shown above, it is recognized properly when the code is running:

Rely on Base Classes for Custom Processors to Read Universal Properties

To read the allowedSites property, the method called must include a public property:

public class HttpRequestBase : HttpRequestProcessor
{
  public string allowedSites {get; set;}
  public abstract void Execute(HttpRequestArgs args);
  public override void Process(HttpRequestArgs args){
    if(Valid()){
      Execute(args);
    }
  }
  public bool Valid(){
    // logic to test Context.Site equality to allowedSites property
  }
}

Above is a stubbed out base class that all processors in this example can inherit.  This ensures the allowedSites property is read properly and the pipeline is or isn’t executed based on the request.  Of note, the implementing class of HttpRequestBase uses the method signature Execute and not Process.

With this approach, when the new brand sites are launched, instead of A) updating some hardcoded values in a class or B) finding every processor that needs updating, we can simply update the brandSites variable with our new site name.  The same approach can be applied to other custom config elements patched into Sitecore.

 

John Rappel

Technical Lead
Tags
  • Configuration
  • Sitecore
subscribe to GeekHive's newsletter

Never miss out on a Sitecore update again!

Our newsletter features a blog roundup of our top posts so you can stay up to date with industry trends, tutorials, and best practices.

Utilize Inheritance on Site Elements

In a multi-site application, care should be given when organizing site definitions.  Many developers seem to forget that sites can inherit from other sites.  Site inheritance provides us with a great tool to organize our site definitions and reduce the overall size of the config.  Site definitions inherit all attributes associated with the inherited config.  If they exclude an attribute, then the attribute value of the inherited config is assumed.

Thusly, it is recommended to create a base site definition  This site will never resolve in SiteResolver processor, yet will help organize all other custom site definitions.  Take the example below:


<!-- brand site definitions -->
<site patch:before="*[@name='website']" 
	name="brand1" 
	inherits="brandBase" 
	hostName="brand1" 
	targetHostName="brand1" 
	rootPath="/sitecore/content/MySites/Brands/Brand1" />
<site patch:before="*[@name='website']" 
	name="brand2" 
	inherits="brandBase" 
	hostName="brand2" 
	targetHostName="brand2" 
	rootPath="/sitecore/content/MySites/Brands/Brand2" />

<!-- marketing site definitions -->
<site patch:before="*[@name='website']" 
	name="marketing1" 
	inherits="marketingBase" 
	hostName="marketing1" 
	targetHostName="marketing1" 
	rootPath="/sitecore/content/MySites/Marketing/ Marketing1" />

<site patch:before="*[@name='website']" 
	name="brandBase" 
	inherits="base" />
<site patch:before="*[@name='website']" 
	htmlCacheSize="200MB" 
	name="marketingBase" 
	inherits="base" />

<site name="base"
	startItem="/home"
	enableTracking="true"
	virtualFolder="/"
	physicalFolder="/"
	database="master"
	domain="extranet"
	allowDebug="true"
	cacheHtml="true"
	htmlCacheSize="50MB"
	registryCacheSize="0"
	viewStateCacheSize="0"
	xslCacheSize="25MB"
	filteredItemsCacheSize="10MB"
	enablePreview="true"
	enableWebEdit="true"
	enableDebugger="true"
	disableClientData="false"
	cacheRenderingParameters="true"
	renderingParametersCacheSize="10MB" />

Note: The base site is intentionally not patched in, and therefore will be appended as the last site on the list of sites.  This is to force it to the bottom of the sites list when viewed on the ShowConfig.aspx page.

Inheritance is as follows:

  • base > brandBase > Brand site
  • base > marketingBase > Marketing site

Notice how the only site definitions with the hostname attribute included are the true site definitions, not the base definitions.  Global settings are defined on the base settings.  In the case of the marketingBase, the htmlCacheSize is increased from 50MB to 200MB.  All marketing sites therefore will have a 200MB html cache.

The base site definition approach makes config transforms much easier as it only takes a single patch to change all site definitions from master to web or change a variety of other settings for certain environments.

Putting it All Together

Sitecore configs may tend to be an afterthought for many developers, but can come back to bite quite hard in a variety of ways: during upgrades, troubleshooting production issues, difficult to reproduce bugs within the Sitecore shell, to name a few.  When done properly, they can provide reassurance that the application is running properly.

Recent Work

Check out what else we've been working on