When building an Enterprise Component Layer (ECL) it’s important to think about how you want your overall solution to be organized. Before we start, I highly recommend you familiarize yourself with a few topics:
- Sitecore Helix – Patterns, Principles and Conventions: Helix is an architectural pattern outlined by Sitecore to help guide developers into organizing their solutions in a consistent and flexible manner. For our purposes, you can think of the Enterprise Layer as our Foundation and Feature modules for our business logic. Each tenant website that we introduce to the CMS should be thought of as living within the Project layer.
- Setting Up a Sitecore Solution – Part 1 Visual Studio and Projects: This blog post outlines how Brainjocks, now Perficient, tackles a tenant solution. There are a few critical yet simple concepts to wrap your head around. Firstly, we separate TDS into multiple projects: Core, Master, and Master.Content. The Core project houses any updates to the Core database that we may introduce, while the Master project houses our Templates, Layouts, Placeholder settings, Rules, and any other elements that we want to ensure are deployed with each and every deployment. The Master.Content project contains elements which we do not want to deploy to managed environments. The Master.Content project simply exists for the developer’s convenience when setting up their local environment. Secondly, we introduce an entire project to house environment configuration. We do not use config transforms, and instead opt for file copy operations (which we’ll automate) to ensure that each environment receives the configuration it needs. Finally, we introduce three class libraries: Web, Custom, and Data. I personally like to consolidate this to only two: Web (for the MVC layer) and Custom (for any business logic or Sitecore extensions), but this is totally up to your own development philosophy. One important note here is that SCORE enables MVC Area support (although newer editions of Sitecore support MVC Areas), and we separate each Tenant into its own MVC Area at the web layer. This ensures separation of concerns, and allows for clean and modular tenant installation.
- Setting Up a Sitecore Solution – Part 2 TDS and Build Configurations: The second part of this blog post outlines how we utilize TDS specifically, and where we install Sitecore. As you’ll see in the video listed below, for local developers each tenant website receives its own Sitecore installation. We also create build profiles and organize the environments project to align with the environments we’ll be deploying to (Integration, QA, Production, etc). The only thing different in 2018 is that Sitecore references can now be pulled from Nuget rather than a local .dll copy.
- SCORE Scaffolding Overview: This video gives you some context on the idea of scaffolding. We need the ability to create a new tenant consistently and quickly. Later on I’ll show you how to get your Enterprise Layer to install in a similar fashion to SCORE for local development.
Multiple Solutions
Problems to Solve
Taking this approach definitely creates some interesting problems. We’ve just introduced the concept of N+1 solutions, where N is the number of brand sites we wish to build. That’s a lot to set up. We’re also deploying to a single website in IIS. Configuration organization becomes a major problem, and this should be managed with an end-state vision in mind. You cannot look at a single solution and infer how you want the application to work – instead, you should think about how you’re going to organize the file system on a multi-site Sitecore application and back it up into your solution layers accordingly.
Configuration Strategy
For example, in this scenario I would argue that the Sitecore CMS owns the web.config for the application. Tenants should not be modifying the web.config without good reason, and the Enterprise Layer likely shouldn’t modify it either. In reality, you as the architect of your CMS own the web.config. We’ll be modifying it for a few things, such as enabling Sitecore.Ship or a robots.txt handler, but in general you should not touch the web.config and you should be very hesitant with any modifications to it. In fact, I don’t even check it into source control.
Additionally, you should never edit a native Sitecore configuration file. Instead, you want to patch in your own modifications to the CMS. The way we do that is introducing .config files to the <sitecore installation directory>/Website/App_Config/Include folder. You’ll notice that this is where Sitecore keeps a large majority of its configuration. You can see the fully calculated configuration by navigating to http://<sitecore>/sitecore/admin/showconfig.aspx. To figure out how the configuration should look, Sitecore is performing a breadth-first search of the App_Config/Include directory. This means that you want your solutions to introduce folders that house the various configuration elements within the App_Config/Include directory that are alphabetically last.
In addition to introducing directories that are alphabetically last, you need to think of how your Tenant and ECL solutions organize their configuration. My general recommendation is to do the following:
- Prepend all config folders with zzz so that they fall alphabetically last.
- Ensure through alphabetical ordering that your Enterprise patches come before your Tenant patches.
- Allow room for Sitecore Support patches, which your ECL solution should house.
- Allow your brands (e.g., Project layers) to overwrite ECL settings. Organize ECL settings so that your Helix Foundation configuration is loaded before your Helix Feature configuration which is loaded before your Tenant Project configuration.
So why shouldn’t I edit Sitecore config files or my web.config?
Upgradability. You lose the ability to easily upgrade and support multiple versions of Sitecore. If you vow to never touch the out-of-the-box CMS configuration files, then the only configuration you need to triage during an upgrade are the ones that you bring to the table. If you start touching Sitecore’s configuration files, how do you keep track of what you’re changing compared to the default settings of the CMS? During an upgrade, understanding what configuration changes are being introduced by your application is critical. Upgrades are already hard enough, do yourself a favor and save yourself some headache up-front.
Configuration across Environments
When drilling into one of the tenant configuration directories, you should see only elements revolving around that tenant.
I personally like to prepend all configuration files with the name of the tenant (or layer from ECL), as this is output via the /sitecore/admin/showconfig.aspx administration page.
This patch:source element can help you identify where a particular configuration element may be coming from, which is absolutely critical in a large scale multisite setup.
Additionally, the Brand.Sites.Environment.config file patches alphabetically after the Brand.Sites.config file. This is by design. Brand.Sites.config should contain a site definition for your tenant that should live across all environments. For the environmental specific elements, you want to modify those within your Brand.Sites.Environment.config file.
For example, your Brand.Sites.config may look like this:
&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt; &amp;lt;configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/"&amp;gt; &amp;lt;sitecore&amp;gt; &amp;lt;events&amp;gt; &amp;lt;event name="publish:end"&amp;gt; &amp;lt;handler type="Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel" method="ClearCache"&amp;gt; &amp;lt;sites hint="list"&amp;gt; &amp;lt;site hint="BrandA"&amp;gt;BrandA&amp;lt;/site&amp;gt; &amp;lt;/sites&amp;gt; &amp;lt;/handler&amp;gt; &amp;lt;/event&amp;gt; &amp;lt;event name="publish:end:remote"&amp;gt; &amp;lt;handler type="Sitecore.Publishing.HtmlCacheClearer, Sitecore.Kernel" method="ClearCache"&amp;gt; &amp;lt;sites hint="list"&amp;gt; &amp;lt;site hint="BrandA"&amp;gt;BrandA&amp;lt;/site&amp;gt; &amp;lt;/sites&amp;gt; &amp;lt;/handler&amp;gt; &amp;lt;/event&amp;gt; &amp;lt;/events&amp;gt; &amp;lt;sites&amp;gt; &amp;lt;site patch:before="site[@name='website']" name="BrandA" virtualFolder="/" physicalFolder="/" requireLogin="false" rootPath="/sitecore/content/BrandA" startItem="/home" database="web" domain="extranet" allowDebug="false" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" language="en" enablePreview="true" enableWebEdit="true" enableDebugger="false" disableClientData="false" searchIndex="branda-web-index" itemNotFound="/sitecore/content/BrandA/home/404" scheme="https" xmlSitemapFilename="_assets/branda-sitemap.xml" robotsTxtItem="/Settings/robotstxt File" /&amp;gt; &amp;lt;/sites&amp;gt; &amp;lt;settings&amp;gt; &amp;lt;setting name="Enterprise.Project.BrandA.SomeSetting" set:value="some-default-value"/&amp;gt; &amp;lt;/settings&amp;gt; &amp;lt;/sitecore&amp;gt; &amp;lt;/configuration&amp;gt;
And your Brand.Sites.Environment.config may look like this:
&amp;lt;configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"&amp;gt; &amp;lt;sitecore&amp;gt; &amp;lt;sites&amp;gt; &amp;lt;site name="BrandA"&amp;gt; &amp;lt;patch:attribute name="hostName"&amp;gt;www.my-local-brand-a.com&amp;lt;/patch:attribute&amp;gt; &amp;lt;patch:attribute name="targetHostName"&amp;gt;www.my-local-brand-a.com&amp;lt;/patch:attribute&amp;gt; &amp;lt;patch:attribute name="cacheHtml"&amp;gt;true&amp;lt;/patch:attribute&amp;gt; &amp;lt;/site&amp;gt; &amp;lt;/sites&amp;gt; &amp;lt;settings&amp;gt; &amp;lt;setting name="Enterprise.Project.BrandA.SomeSetting" set:value="some-environment-specific-value"/&amp;gt; &amp;lt;/settings&amp;gt; &amp;lt;/sitecore&amp;gt; &amp;lt;/configuration&amp;gt;
Tenant Assets
One of the best ways to handle local tenant assets is through enabling MVC Area support for Sitecore. SCORE helped us enable it historically, but in recent versions of Sitecore you can enable it through CMS configuration directly.
This is an incredibly powerful capability of ASP.NET MVC in this context. It allows us to separate each brand into its own isolated collection of CSS, JavaScript, Thumbnail Images, Layout definitions, and more. In doing this, you allow for multiple teams to work on multiple brands in parallel, as each team can be an autonomous unit. Each brand can select its own front-end frameworks accordingly, and they all install alongside each other cleanly.
The Enterprise Solution
When it comes to effective organization of the Enterprise solution, I recommend you follow Helix principles. I also recommend you study Brian’s and SCORE’s default solution structure to get an idea of how to effectively handle environment configurations and TDS setup. For the enterprise layer, I like to follow a path similar to this:
Here, we’ve done a few things. We’ve separated the solution into multiple logical directories. We have root level folders for Configuration, Foundation, Feature, and Project layers. The root level .tds folder contains a global TDS settings file which all TDS projects reference. Foundation and Feature are for the logical Helix layers that they represent. Foundational level projects shouldn’t have any components or other MVC elements, while the Feature projects represent logical components and extensions to the web layer that you need to build. Each Feature level project has an MVC Area called Enterprise
, and all projects compile down to the same Area for production. Each Feature also has the option of bringing in configuration files, but anything environment specific is reserved for the Enterprise.Environments Configuration project. The Project layer contains our Scaffolding which we’ll get into in a later post. Finally, the Configuration layer contains any Sitecore patches that we need, an Enterprise.Environment project for environmental configs following Brian’s solution setup above, and an Enterprise.Package project which is used to generate a Nuget package of this Enterprise Component Layer. Distribution of this Enterprise Layer via Nuget is critical for scaffolding. As you saw in Brian’s video above regarding a SCORE installation, we’re going to set this solution up to distribute and install itself in the same way for local developers working at the Tenant layer.
Notice that each Foundation and Feature project also has a TDS counterpart. For any logical items which may be required for this module, we can include these within the TDS project. We also call out the Source Web Project(s)
setting in TDS to include the output of the project in compilation. For instance, on the Enterprise.Feature.Events.Master
TDS project, we call out the Enterprise.Feature.Events
C# project as the Source Web Project:
Then on the Enterprise.Configuration.Master
(not to be confused with Enterprise.Foundation.Configuration.Master*
) TDS project, we call out Package Bundling
and Update Package
file generation like so:
With Generate Package During Build
enabled, we are instructing TDS to create a Sitecore .update
package that represents the logical contents of the TDS Project along with the compilation output of its Source Web Project. With Package Bundling
enabled, a compilation will also include the contents of the bundled TDS projects as well as the output of each bundled TDS project’s Source Web Project output. This means that a compilation of the entire solution, from a DevOps point of view, produces a single** .update
file artifact which can be used in automated deployments.
* The Enterprise.Configuration collection of projects is intended to house configuration elements for the various layers. It also features a packaging mechanism for Nuget. The Enterprise.Foundation.Configuration project is a foundation module which other modules utilize to extract configuration settings from configuration files or Sitecore items accordingly.
** In reality, we generate a single .update package for the Core database, a single .update package for the Master database along with Content Management specific file-system assets, and a single .update package for Content Delivery specific file-system assets.
A few additional resources
As you can also see, each of our projects outputs a Unit Test project. We want as much unit testing as is reasonably possible on our Enterprise Layer. If you’re struggling with Unit Testing in Sitecore, I highly recommend you check out Pavel Veller’s introduction to FakeDB.
Also, it can be quite difficult to create Helix projects that line up correctly. There are lots of interconnected settings that can be easily missed. To remedy this, I recommend you check out some automation tips from Marc Duiker and the rest of the community. Automation is the key. You can do this with something like Yeoman, with Powershell scripts, with Project templates, etc – get creative, and use what works!
An interesting article to read. What was the level of difficulty in created the helix solution and challenges?
If I recall, we ran into a few issues that needed solving. Firstly, Helix is a complicated solution structure. There are a lot of projects that need to be set up, and this is really difficult to do by hand. We ended up using a ton of automation to help create new modules- we landed on the Powershell style automation, but others have used tools such as Yeoman. Another issue we faced was around TDS projects- we couldn’t find a clean way to automate the creation of these, so that led to a bit of pain. If you are using Unicorn, you may have a different experience. We ended up creating a Visual Studio template for our TDS project creation. Also, managing the list of modules within our Nuget package configuration was difficult- it was a checklist item for our pull requests.
Another issue that I see a lot with Helix is the over-use of modules. Some very simple features sometimes end up with their own foundation/feature layer for just a few classes. I like to consolidate some of these things into common modules to make it a bit easier in the long run. For instance, if you are going to customize your LinkManager, I don’t think it makes sense to bake that into a dedicated Foundation module.
Other than that, compilation and Visual Studio start up times were longer than I would like.